Compare commits
314 Commits
v1.5.0
...
release/1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0df3f73ae | ||
|
|
581f3c3d58 | ||
|
|
6600c430ab | ||
|
|
807eb2ea7f | ||
|
|
7dea6fc1b7 | ||
|
|
bd306e9400 | ||
|
|
3e686211e1 | ||
|
|
6d1b4b0a39 | ||
|
|
58938fc07c | ||
|
|
96dd4e02f3 | ||
|
|
411c537438 | ||
|
|
bbd112280a | ||
|
|
28d074366e | ||
|
|
11d76656de | ||
|
|
1c96bc31d5 | ||
|
|
0030064f55 | ||
|
|
4726f5c136 | ||
|
|
c7fafedc51 | ||
|
|
3eeb70ae36 | ||
|
|
29b1a3dca3 | ||
|
|
caf844b5fb | ||
|
|
6b7bdf8c4f | ||
|
|
aad433a3bc | ||
|
|
3f1bb56826 | ||
|
|
b2b3add616 | ||
|
|
2d484d4a8e | ||
|
|
2f252f19c9 | ||
|
|
a27f372785 | ||
|
|
f074e642ec | ||
|
|
217ed905d4 | ||
|
|
b920efc955 | ||
|
|
330fadbea9 | ||
|
|
50c595e3d6 | ||
|
|
26f258c6cf | ||
|
|
f15a72e59d | ||
|
|
8accaae6b1 | ||
|
|
d4259501af | ||
|
|
fd5d5ae98e | ||
|
|
457901ff82 | ||
|
|
e201be1c65 | ||
|
|
acde14372d | ||
|
|
79988a2325 | ||
|
|
784f6e703c | ||
|
|
29b157f287 | ||
|
|
c030bd35ca | ||
|
|
06fe076ce2 | ||
|
|
ae6cba067c | ||
|
|
72ae19a95d | ||
|
|
1f889be07a | ||
|
|
39061b659a | ||
|
|
d38f29ac7c | ||
|
|
1a8e67f4de | ||
|
|
8265c302ad | ||
|
|
110d7c6acf | ||
|
|
244b767f8f | ||
|
|
f40950efc9 | ||
|
|
0e0534c273 | ||
|
|
9b3ea3656f | ||
|
|
62b2a367ff | ||
|
|
ab9dd32902 | ||
|
|
43fc498297 | ||
|
|
ef3eee7873 | ||
|
|
9f0deea9dd | ||
|
|
e3798600ed | ||
|
|
00834cd5e0 | ||
|
|
ed35c4f74e | ||
|
|
9cd3e2d494 | ||
|
|
3345f48986 | ||
|
|
b611d63975 | ||
|
|
fb3866aa1a | ||
|
|
a9f131b645 | ||
|
|
e5728662c5 | ||
|
|
94a97fb0fd | ||
|
|
b5bea6fe7a | ||
|
|
fb9d677d76 | ||
|
|
7c4fc7bd0d | ||
|
|
de992cecf3 | ||
|
|
cd94549606 | ||
|
|
214a6eb5ce | ||
|
|
db5f0aa02d | ||
|
|
ba48ab3659 | ||
|
|
d1538e07d3 | ||
|
|
fe0c033b2d | ||
|
|
2e58dca048 | ||
|
|
d38ab8a439 | ||
|
|
acd7b9ba8c | ||
|
|
56f72b225c | ||
|
|
8bfaf7425a | ||
|
|
77a8726a03 | ||
|
|
119fea3379 | ||
|
|
e54e0d6511 | ||
|
|
a2a88cfafa | ||
|
|
5ff53d08ed | ||
|
|
0ddda4a668 | ||
|
|
d3a76e9f2f | ||
|
|
ea7ec2b5fc | ||
|
|
b9b4ccb180 | ||
|
|
2f15d410fe | ||
|
|
88f5af3e77 | ||
|
|
454ca27c54 | ||
|
|
f536cb3536 | ||
|
|
e6ba7379eb | ||
|
|
f6b01b6e02 | ||
|
|
ce27f8e89c | ||
|
|
a52635f940 | ||
|
|
b608125545 | ||
|
|
631cded0d6 | ||
|
|
43b5140754 | ||
|
|
557a05135e | ||
|
|
618416d0d2 | ||
|
|
9a4ee3db69 | ||
|
|
999dde3fa4 | ||
|
|
1171cce550 | ||
|
|
77e13338ad | ||
|
|
fd35b5ea72 | ||
|
|
f98f25fb6b | ||
|
|
511a49041f | ||
|
|
74be5cfe96 | ||
|
|
1f54b36ece | ||
|
|
d12b77b572 | ||
|
|
4928234785 | ||
|
|
208e3c9933 | ||
|
|
d697381d8b | ||
|
|
cd6b1a2327 | ||
|
|
ff21380099 | ||
|
|
a773531003 | ||
|
|
23ecd43885 | ||
|
|
3415bf5cd3 | ||
|
|
45b9f1190f | ||
|
|
ef1b09671a | ||
|
|
ee282af53e | ||
|
|
455a95d46c | ||
|
|
76666b0d22 | ||
|
|
45fd43682a | ||
|
|
fd801e3323 | ||
|
|
429c6ebb1b | ||
|
|
ea2f24fe23 | ||
|
|
db4a2cfaac | ||
|
|
583223f454 | ||
|
|
f9fcc16f54 | ||
|
|
50ca6ee63d | ||
|
|
56338be13e | ||
|
|
b9ec5ea83c | ||
|
|
389585c47a | ||
|
|
e9583087eb | ||
|
|
57e2090d70 | ||
|
|
5fbf26b8cb | ||
|
|
447c728557 | ||
|
|
a3ca4c81ae | ||
|
|
fb398a5520 | ||
|
|
9a9bb92f91 | ||
|
|
e23a5c24d6 | ||
|
|
1a42a54d98 | ||
|
|
5c91352bae | ||
|
|
3428ea2f18 | ||
|
|
24e5d337a6 | ||
|
|
a2c1413036 | ||
|
|
bab092f04b | ||
|
|
2bf4e6c5c6 | ||
|
|
584add97a3 | ||
|
|
57143a434e | ||
|
|
e31bd7600c | ||
|
|
f02ec8b24b | ||
|
|
b8704f980f | ||
|
|
3accf74687 | ||
|
|
a213ca746c | ||
|
|
349e306d38 | ||
|
|
ca1b1032eb | ||
|
|
a6c9fb0f8b | ||
|
|
c8230c55ee | ||
|
|
55f77613d4 | ||
|
|
c9a1ff45c7 | ||
|
|
c209f66d49 | ||
|
|
3efa02eb81 | ||
|
|
8506f66236 | ||
|
|
cb2826f171 | ||
|
|
0990c9cc3d | ||
|
|
4aa9594a61 | ||
|
|
ed208cf433 | ||
|
|
428faeb756 | ||
|
|
e858edd85c | ||
|
|
e4ab27a292 | ||
|
|
eece5793d6 | ||
|
|
3df737a94f | ||
|
|
0e4c414c2e | ||
|
|
326304db54 | ||
|
|
c8e54524a3 | ||
|
|
d671060a47 | ||
|
|
93dab76da2 | ||
|
|
bbed8e5fae | ||
|
|
e16f8fc7e9 | ||
|
|
86f17094bb | ||
|
|
b1b49758b1 | ||
|
|
4790665759 | ||
|
|
8ede492cba | ||
|
|
5f607cc034 | ||
|
|
3b9f508be9 | ||
|
|
89e381b7ea | ||
|
|
57869b2145 | ||
|
|
46976900d7 | ||
|
|
a1535da117 | ||
|
|
f43d782b5c | ||
|
|
5c443e2f93 | ||
|
|
54f01f63f7 | ||
|
|
c64b4473e4 | ||
|
|
d413a37c1f | ||
|
|
202fb12008 | ||
|
|
59dea63870 | ||
|
|
9a18f2b553 | ||
|
|
4293ec3805 | ||
|
|
f3b616e495 | ||
|
|
003ea24990 | ||
|
|
92d4566a54 | ||
|
|
70a933edc1 | ||
|
|
d2d77f28aa | ||
|
|
39179971c5 | ||
|
|
c06f36e8c2 | ||
|
|
1d2d9d8b99 | ||
|
|
5f529817ef | ||
|
|
7c91bc2f37 | ||
|
|
ef022f5a6d | ||
|
|
54ce00c8b9 | ||
|
|
baabbfb1ea | ||
|
|
28e676ac9a | ||
|
|
21fac9ec7a | ||
|
|
335955820b | ||
|
|
d2b0e7209f | ||
|
|
f8ed21c819 | ||
|
|
9c2143effe | ||
|
|
8a3fa6aff6 | ||
|
|
7e304bb231 | ||
|
|
99d614289e | ||
|
|
b90894c20f | ||
|
|
921834c917 | ||
|
|
83df4451e6 | ||
|
|
2ad9e1bb43 | ||
|
|
c9990e5ca4 | ||
|
|
c95e61db09 | ||
|
|
7bb12ff0ec | ||
|
|
670bfa18de | ||
|
|
130f619b05 | ||
|
|
f900c842cb | ||
|
|
c2844a8f35 | ||
|
|
a864dabbaf | ||
|
|
02786f4801 | ||
|
|
5a4fe266c6 | ||
|
|
b30a4db0b8 | ||
|
|
2e76e07764 | ||
|
|
1be92f5078 | ||
|
|
8afff29cd4 | ||
|
|
9c6090a355 | ||
|
|
27b73227ed | ||
|
|
9582f8380f | ||
|
|
bfeac7e70b | ||
|
|
2050c0b7a8 | ||
|
|
144f9bed69 | ||
|
|
f2b642d944 | ||
|
|
bcdc75953e | ||
|
|
1fabe5a7cf | ||
|
|
fe9a4b7aa3 | ||
|
|
56735dc1c6 | ||
|
|
42287b92f1 | ||
|
|
7d9e642f24 | ||
|
|
b20e10585f | ||
|
|
8438b211a6 | ||
|
|
f94314afec | ||
|
|
4584d23434 | ||
|
|
2791501781 | ||
|
|
6ea798e55b | ||
|
|
0ab6ac569e | ||
|
|
f91d7352a4 | ||
|
|
d644b8fe01 | ||
|
|
5a8042cc10 | ||
|
|
ae910eb731 | ||
|
|
c1158c3175 | ||
|
|
79562e7ad9 | ||
|
|
e3388bea96 | ||
|
|
a3b4a7ef1d | ||
|
|
947b06cd61 | ||
|
|
a3f3561f02 | ||
|
|
48095d38be | ||
|
|
1c6858653a | ||
|
|
648797325e | ||
|
|
af3fa88d67 | ||
|
|
8123effa65 | ||
|
|
554800c06f | ||
|
|
687ce29366 | ||
|
|
c70301572c | ||
|
|
714f58e2c5 | ||
|
|
9bf9dca88a | ||
|
|
0663f25208 | ||
|
|
275d162b81 | ||
|
|
675956a7c4 | ||
|
|
cf1602c0af | ||
|
|
35979ed332 | ||
|
|
6e65ae5306 | ||
|
|
95e716b8ce | ||
|
|
554284ac67 | ||
|
|
fa6102bbad | ||
|
|
f28d5f19a7 | ||
|
|
34d59c7741 | ||
|
|
21d432a3ca | ||
|
|
d444935140 | ||
|
|
9de9d96e35 | ||
|
|
5932558ca2 | ||
|
|
7b22adb72e | ||
|
|
0db5d062be | ||
|
|
678d510e29 | ||
|
|
1fc3307d22 | ||
|
|
45c17ba949 | ||
|
|
15bd1d9006 | ||
|
|
b4715f0931 | ||
|
|
6f24a2a88c | ||
|
|
fcbecf895a |
@@ -8,6 +8,8 @@ tests:
|
||||
- XDG_CACHE_HOME=/cache bash .travis.sh tests
|
||||
tags:
|
||||
- python3
|
||||
except:
|
||||
- pypi
|
||||
pypi:
|
||||
stage: release
|
||||
script:
|
||||
@@ -22,7 +24,7 @@ pypi:
|
||||
tags:
|
||||
- python3
|
||||
only:
|
||||
- release
|
||||
- pypi
|
||||
artifacts:
|
||||
paths:
|
||||
- src/dist/
|
||||
|
||||
42
.travis.yml
@@ -12,29 +12,29 @@ services:
|
||||
- postgresql
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/sqlite.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/sqlite.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/sqlite.cfg
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.4
|
||||
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
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=style
|
||||
- python: 3.6
|
||||
env: JOB=plugins
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests-cov
|
||||
- python: 3.6
|
||||
env: JOB=style
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg
|
||||
- python: 3.4
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.5
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.6
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_mysql.cfg
|
||||
- python: 3.4
|
||||
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
|
||||
env: JOB=tests PRETIX_CONFIG_FILE=tests/travis_postgres.cfg
|
||||
- python: 3.6
|
||||
env: JOB=plugins
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
|
||||
36
doc/_themes/pretix_theme/static/css/pretix.css
vendored
@@ -6063,3 +6063,39 @@ url('../opensans_regular_macroman/OpenSans-Regular-webfont.svg#open_sansregular'
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
img.screenshot, a.screenshot img {
|
||||
box-shadow: 0 4px 18px 0 rgba(0,0,0,0.1), 0 6px 20px 0 rgba(0,0,0,0.09);
|
||||
}
|
||||
|
||||
/* Changes */
|
||||
.versionchanged {
|
||||
background: #e7f2fa;
|
||||
padding: 12px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 24px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.versionmodified {
|
||||
background: #6ab0de;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
color: #fff;
|
||||
margin: -12px;
|
||||
padding: 6px 12px;
|
||||
margin-bottom: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.versionmodified:before {
|
||||
font-family: "FontAwesome";
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
text-decoration: inherit;
|
||||
content: "";
|
||||
margin-right: 4px;
|
||||
}
|
||||
.versionchanged p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ at the following locations. It will try to read the file from the specified path
|
||||
the following order. The file that is found *last* will override the settings from
|
||||
the files found before.
|
||||
|
||||
1. ``/etc/pretix/pretix.cfg``
|
||||
2. ``~/.pretix.cfg``
|
||||
3. ``pretix.cfg`` in the current working directory
|
||||
1. ``PREFIX_CONFIG_FILE`` environment variable
|
||||
2. ``/etc/pretix/pretix.cfg``
|
||||
3. ``~/.pretix.cfg``
|
||||
4. ``pretix.cfg`` in the current working directory
|
||||
|
||||
The file is expected to be in the INI format as specified in the `Python documentation`_.
|
||||
|
||||
@@ -59,6 +60,14 @@ Example::
|
||||
``password_reset``
|
||||
Enables or disables password reset. Defaults to ``on``.
|
||||
|
||||
``long_sessions``
|
||||
Enables or disables the "keep me logged in" button. Defaults to ``on``.
|
||||
|
||||
``ecb_rates``
|
||||
By default, pretix periodically downloads a XML file from the European Central Bank to retrieve exchange rates
|
||||
that are used to print tax amounts in the customer currency on invoices for some currencies. Set to ``off`` to
|
||||
disable this feature. Defaults to ``on``.
|
||||
|
||||
|
||||
Locale settings
|
||||
---------------
|
||||
@@ -163,14 +172,9 @@ Django settings
|
||||
Example::
|
||||
|
||||
[django]
|
||||
hosts=localhost
|
||||
secret=j1kjps5a5&4ilpn912s7a1!e2h!duz^i3&idu@_907s$wrz@x-
|
||||
debug=off
|
||||
|
||||
``hosts``
|
||||
Comma-separated list of allowed host names for this installation.
|
||||
Default: ``localhost``
|
||||
|
||||
``secret``
|
||||
The secret to be used by Django for signing and verification purposes. If this
|
||||
setting is not provided, pretix will generate a random secret on the first start
|
||||
|
||||
@@ -4,6 +4,8 @@ Basic concepts
|
||||
This page describes basic concepts and definition that you need to know to interact
|
||||
with pretix' REST API, such as authentication, pagination and similar definitions.
|
||||
|
||||
.. _`rest-auth`:
|
||||
|
||||
Obtaining an API token
|
||||
----------------------
|
||||
|
||||
@@ -13,12 +15,14 @@ or choose an existing team that has the level of permissions the token should ha
|
||||
create a new token using the form below the list of team members:
|
||||
|
||||
.. image:: img/token_form.png
|
||||
:class: screenshot
|
||||
|
||||
You can enter a description for the token to distinguish from other tokens later on.
|
||||
Once you click "Add", you will be provided with an API token in the success message.
|
||||
Copy this token, as you won't be able to retrieve it again.
|
||||
|
||||
.. image:: img/token_success.png
|
||||
:class: screenshot
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
@@ -24,8 +24,14 @@ is_public boolean If ``true``, th
|
||||
presale_start datetime The date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The event location (or ``null``)
|
||||
has_subevents boolean ``True`` if the event series feature is active for this
|
||||
event
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The ``meta_data`` field has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -67,6 +73,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -109,6 +117,8 @@ Endpoints
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"has_subevents": false,
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
|
||||
@@ -6,6 +6,8 @@ Resources and endpoints
|
||||
|
||||
organizers
|
||||
events
|
||||
subevents
|
||||
taxrules
|
||||
categories
|
||||
items
|
||||
questions
|
||||
|
||||
@@ -11,7 +11,7 @@ The invoice resource contains the following public fields:
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
invoice_no string Invoice number (without prefix)
|
||||
number string Invoice number (with prefix)
|
||||
order string Order code of the order this invoice belongs to
|
||||
is_cancellation boolean ``True``, if this invoice is the cancellation of a
|
||||
different invoice.
|
||||
@@ -29,12 +29,34 @@ payment_provider_text string Text to be prin
|
||||
footer_text string Text to be printed in the page footer area
|
||||
lines list of objects The actual invoice contents
|
||||
├ description string Text representing the invoice line (e.g. product name)
|
||||
├ gross_value money (string) Price including VAT
|
||||
├ tax_value money (string) VAT amount
|
||||
└ tax_rate decimal (string) Used VAT rate
|
||||
├ gross_value money (string) Price including taxes
|
||||
├ tax_value money (string) Tax amount included
|
||||
├ tax_name string Name of used tax rate (e.g. "VAT")
|
||||
└ tax_rate decimal (string) Used tax rate
|
||||
foreign_currency_display string If the invoice should also show the total and tax
|
||||
amount in a different currency, this contains the
|
||||
currency code (``null`` otherwise).
|
||||
foreign_currency_rate decimal (string) If ``foreign_currency_rate`` is set and the system
|
||||
knows the exchange rate to the event currency at
|
||||
invoicing time, it is stored here.
|
||||
foreign_currency_rate_date date If ``foreign_currency_rate`` is set, this signifies the
|
||||
date at which the currency rate was obtained.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
|
||||
The attribute ``invoice_no`` has been dropped in favor of ``number`` which includes the number including the prefix,
|
||||
since the prefix can now vary. Also, invoices now need to be identified by their ``number`` instead of the raw
|
||||
number.
|
||||
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attributes ``lines.tax_name``, ``foreign_currency_display``, ``foreign_currency_rate``, and
|
||||
``foreign_currency_rate_date`` have been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -64,7 +86,7 @@ Endpoints
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"invoice_no": "00001",
|
||||
"number": "SAMPLECONF-00001",
|
||||
"order": "ABC12",
|
||||
"is_cancellation": false,
|
||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
||||
@@ -81,9 +103,13 @@ Endpoints
|
||||
"description": "Budget Ticket",
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
"tax_rate": "0.00"
|
||||
}
|
||||
]
|
||||
],
|
||||
"foreign_currency_display": "PLN",
|
||||
"foreign_currency_rate": "4.2408",
|
||||
"foreign_currency_rate_date": "2017-07-24"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -95,14 +121,14 @@ Endpoints
|
||||
:query string refers: If set, only invoices refering to the given invoice will be returned.
|
||||
:query string locale: If set, only invoices with the given locale will be returned.
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``date`` and
|
||||
``invoice_no``. Default: ``invoice_no``
|
||||
``nr`` (equals to ``number``). Default: ``nr``
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(invoice_no)/
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/invoices/(number)/
|
||||
|
||||
Returns information on one invoice, identified by its invoice number.
|
||||
|
||||
@@ -110,7 +136,7 @@ Endpoints
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/00001/ HTTP/1.1
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/invoices/SAMPLECONF-00001/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
@@ -123,7 +149,7 @@ Endpoints
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"invoice_no": "00001",
|
||||
"number": "SAMPLECONF-00001",
|
||||
"order": "ABC12",
|
||||
"is_cancellation": false,
|
||||
"invoice_from": "Big Events LLC\nDemo street 12\nDemo town",
|
||||
@@ -140,9 +166,13 @@ Endpoints
|
||||
"description": "Budget Ticket",
|
||||
"gross_value": "23.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_name": "VAT",
|
||||
"tax_rate": "0.00"
|
||||
}
|
||||
]
|
||||
],
|
||||
"foreign_currency_display": "PLN",
|
||||
"foreign_currency_rate": "4.2408",
|
||||
"foreign_currency_rate_date": "2017-07-24"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
|
||||
@@ -27,6 +27,7 @@ free_price boolean If ``True``, cu
|
||||
lower than the price defined by ``default_price`` or
|
||||
otherwise).
|
||||
tax_rate decimal (string) The VAT rate to be applied for this item.
|
||||
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
|
||||
(such as add-ons or merchandise).
|
||||
@@ -49,6 +50,9 @@ min_per_order integer This product ca
|
||||
max_per_order integer This product can only be bought if it is included at
|
||||
most this many times in the order (or ``null`` for no
|
||||
limitation).
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
that this ticket requires special attention if such
|
||||
a product is being scanned.
|
||||
has_variations boolean Shows whether or not this item has variations
|
||||
(read-only).
|
||||
variations list of objects A list with one object for each variation of this item.
|
||||
@@ -70,6 +74,11 @@ addons list of objects Definition of a
|
||||
└ position integer An integer, used for sorting
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attribute ``tax_rule`` has been added. ``tax_rate`` is kept for compatibility. The attribute
|
||||
``checkin_attention`` has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
@@ -108,6 +117,7 @@ Endpoints
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
@@ -118,6 +128,7 @@ Endpoints
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": false,
|
||||
"variations": [
|
||||
{
|
||||
@@ -188,6 +199,7 @@ Endpoints
|
||||
"description": null,
|
||||
"free_price": false,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": 1,
|
||||
"admission": false,
|
||||
"position": 0,
|
||||
"picture": null,
|
||||
@@ -198,6 +210,7 @@ Endpoints
|
||||
"allow_cancel": true,
|
||||
"min_per_order": null,
|
||||
"max_per_order": null,
|
||||
"checkin_attention": false,
|
||||
"has_variations": false,
|
||||
"variations": [
|
||||
{
|
||||
|
||||
@@ -27,20 +27,38 @@ expires datetime The order will
|
||||
payment_date date Date of payment receival
|
||||
payment_provider string Payment provider used for this order
|
||||
payment_fee money (string) Payment fee included in this order's total
|
||||
payment_fee_tax_rate decimal (string) VAT rate applied to the payment fee
|
||||
payment_fee_tax_value money (string) VAT value included in the payment fee
|
||||
payment_fee_tax_rate decimal (string) Tax rate applied to the payment fee
|
||||
payment_fee_tax_value money (string) Tax value included in the payment fee
|
||||
payment_fee_tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
total money (string) Total value of this order
|
||||
comment string Internal comment on this order
|
||||
invoice_address object Invoice address information (can be ``null``)
|
||||
├ last_modified datetime Last modification date of the address
|
||||
├ company string Customer company name
|
||||
├ is_business boolean Business or individual customers (always ``False``
|
||||
for orders created before pretix 1.7, do not rely on
|
||||
it).
|
||||
├ name string Customer name
|
||||
├ street string Customer street
|
||||
├ zipcode string Customer ZIP code
|
||||
├ city string Customer city
|
||||
├ country string Customer country
|
||||
└ vat_id string Customer VAT ID
|
||||
├ vat_id string Customer VAT ID
|
||||
└ vat_id_validated string ``True``, if the VAT ID has been validated against the
|
||||
EU VAT service and validation was successful. This only
|
||||
happens in rare cases.
|
||||
position list of objects List of order positions (see below)
|
||||
fees list of objects List of fees included in the order total (i.e.
|
||||
payment fees)
|
||||
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
|
||||
``other``)
|
||||
├ value money (string) Fee amount
|
||||
├ description string Human-readable string with more details (can be empty)
|
||||
├ internal_type string Internal string (i.e. ID of the payment provider),
|
||||
can be empty
|
||||
├ tax_rate decimal (string) VAT rate applied for this fee
|
||||
├ tax_value money (string) VAT included in this fee
|
||||
└ tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
downloads list of objects List of ticket download options for order-wise ticket
|
||||
downloading. This might be a multi-page PDF or a ZIP
|
||||
file of tickets for outputs that do not support
|
||||
@@ -50,6 +68,19 @@ downloads list of objects List of ticket
|
||||
└ url string Download URL
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
.. versionchanged:: 1.6
|
||||
|
||||
The ``invoice_address.country`` attribute contains a two-letter country code for all new orders. For old orders,
|
||||
a custom text might still be returned.
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
|
||||
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
|
||||
deprecated in favour of the new ``fees`` attribute but will still be served and removed in 1.9.
|
||||
|
||||
|
||||
Order position resource
|
||||
-----------------------
|
||||
|
||||
@@ -69,8 +100,10 @@ attendee_email string Specified atten
|
||||
voucher integer Internal ID of the voucher used for this position (or ``null``)
|
||||
tax_rate decimal (string) VAT rate applied for this position
|
||||
tax_value money (string) VAT included in this position
|
||||
tax_rule integer The ID of the used tax rule (or ``null``)
|
||||
secret string Secret code printed on the tickets for validation
|
||||
addon_to integer Internal ID of the position this position is an add-on for (or ``null``)
|
||||
subevent integer ID of the date inside an event series this position belongs to (or ``null``).
|
||||
checkins list of objects List of check-ins with this ticket
|
||||
└ datetime datetime Time of check-in
|
||||
downloads list of objects List of ticket download options
|
||||
@@ -82,6 +115,10 @@ answers list of objects Answers to user
|
||||
└ options list of integers Internal IDs of selected option(s)s (only for choice types)
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The attribute ``tax_rule`` has been added.
|
||||
|
||||
|
||||
Order endpoints
|
||||
---------------
|
||||
@@ -121,20 +158,20 @@ Order endpoints
|
||||
"expires": "2017-12-10T10:00:00Z",
|
||||
"payment_date": "2017-12-05",
|
||||
"payment_provider": "banktransfer",
|
||||
"payment_fee": "0.00",
|
||||
"payment_fee_tax_rate": "0.00",
|
||||
"payment_fee_tax_value": "0.00",
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"is_business": True,
|
||||
"company": "Sample company",
|
||||
"name": "John Doe",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"vat_id": "EU123456789"
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
@@ -149,8 +186,10 @@ Order endpoints
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_rule": null,
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
@@ -224,20 +263,20 @@ Order endpoints
|
||||
"expires": "2017-12-10T10:00:00Z",
|
||||
"payment_date": "2017-12-05",
|
||||
"payment_provider": "banktransfer",
|
||||
"payment_fee": "0.00",
|
||||
"payment_fee_tax_rate": "0.00",
|
||||
"payment_fee_tax_value": "0.00",
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"company": "Sample company",
|
||||
"is_business": True,
|
||||
"name": "John Doe",
|
||||
"street": "Test street 12",
|
||||
"zipcode": "12345",
|
||||
"city": "Testington",
|
||||
"country": "Testikistan",
|
||||
"vat_id": "EU123456789"
|
||||
"vat_id": "EU123456789",
|
||||
"vat_id_validated": False
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
@@ -251,9 +290,11 @@ Order endpoints
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
@@ -369,9 +410,11 @@ Order position endpoints
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
@@ -407,6 +450,7 @@ Order position endpoints
|
||||
:query string order__status: Only return positions with the given order status.
|
||||
:query bollean has_checkin: If set to ``true`` or ``false``, only return positions that have or have not been
|
||||
checked in already.
|
||||
:query integer subevent: Only return positions of the sub-event with the given ID
|
||||
:query integer addon_to: Only return positions that are add-ons to the position with the given ID.
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
@@ -445,9 +489,11 @@ Order position endpoints
|
||||
"attendee_email": null,
|
||||
"voucher": null,
|
||||
"tax_rate": "0.00",
|
||||
"tax_rule": null,
|
||||
"tax_value": "0.00",
|
||||
"secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
"addon_to": null,
|
||||
"subevent": null,
|
||||
"checkins": [
|
||||
{
|
||||
"datetime": "2017-12-25T12:45:23Z"
|
||||
|
||||
@@ -22,6 +22,7 @@ type string The expected ty
|
||||
* ``B`` – boolean
|
||||
* ``C`` – choice from a list
|
||||
* ``M`` – multiple choice from a list
|
||||
* ``F`` – file upload
|
||||
required boolean If ``True``, the question needs to be filled out.
|
||||
position integer An integer, used for sorting
|
||||
items list of integers List of item IDs this question is assigned to.
|
||||
|
||||
@@ -17,6 +17,7 @@ name string The internal na
|
||||
size integer The size of the quota or ``null`` for unlimited
|
||||
items list of integers List of item IDs this quota acts on.
|
||||
variations list of integers List of item variation IDs this quota acts on.
|
||||
subevent integer ID of the date inside an event series this quota belongs to (or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -53,7 +54,8 @@ Endpoints
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [1, 2],
|
||||
"variations": [1, 4, 5, 7]
|
||||
"variations": [1, 4, 5, 7],
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -61,6 +63,7 @@ Endpoints
|
||||
: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 ``id`` and ``position``.
|
||||
Default: ``position``
|
||||
:query integer subevent: Only return quotas of the sub-event with the given ID
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
@@ -92,7 +95,8 @@ Endpoints
|
||||
"name": "Ticket Quota",
|
||||
"size": 200,
|
||||
"items": [1, 2],
|
||||
"variations": [1, 4, 5, 7]
|
||||
"variations": [1, 4, 5, 7],
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
|
||||
144
doc/api/resources/subevents.rst
Normal file
@@ -0,0 +1,144 @@
|
||||
Event series dates / Sub-events
|
||||
===============================
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Events can represent whole event series if the ``has_subevents`` property of the event is active.
|
||||
In this case, many other resources are additionally connected to an event date (also called sub-event).
|
||||
The sub-event resource contains the following public fields:
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the sub-event
|
||||
name multi-lingual string The sub-event's full name
|
||||
active boolean If ``true``, the sub-event ticket shop is publicly
|
||||
available.
|
||||
date_from datetime The sub-event's start date
|
||||
date_to datetime The sub-event's end date (or ``null``)
|
||||
date_admission datetime The sub-event's admission date (or ``null``)
|
||||
presale_start datetime The sub-date at which the ticket shop opens (or ``null``)
|
||||
presale_end datetime The sub-date at which the ticket shop closes (or ``null``)
|
||||
location multi-lingual string The sub-event location (or ``null``)
|
||||
item_price_overrides list of objects List of items for which this sub-event overrides the
|
||||
default price
|
||||
├ item integer The internal item ID
|
||||
└ price money (string) The price or ``null`` for the default price
|
||||
variation_price_overrides list of objects List of variations for which this sub-event overrides
|
||||
the default price
|
||||
├ variation integer The internal variation ID
|
||||
└ price money (string) The price or ``null`` for the default price
|
||||
meta_data dict Values set for organizer-specific meta data parameters.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The ``meta_data`` field has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/
|
||||
|
||||
Returns a list of all sub-events of an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "First Sample Conference"},
|
||||
"active": false,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": null,
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"meta_data": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/subevents/(id)/
|
||||
|
||||
Returns information on one sub-event, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/subevents/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "First Sample Conference"},
|
||||
"active": false,
|
||||
"date_from": "2017-12-27T10:00:00Z",
|
||||
"date_to": null,
|
||||
"date_admission": null,
|
||||
"presale_start": null,
|
||||
"presale_end": null,
|
||||
"location": null,
|
||||
"item_price_overrides": [
|
||||
{
|
||||
"item": 2,
|
||||
"price": "12.00"
|
||||
}
|
||||
],
|
||||
"variation_price_overrides": [],
|
||||
"meta_data": {}
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``slug`` field of the sub-event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
109
doc/api/resources/taxrules.rst
Normal file
@@ -0,0 +1,109 @@
|
||||
Tax rules
|
||||
=========
|
||||
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Tax rules specify how tax should be calculated for specific products.
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
===================================== ========================== =======================================================
|
||||
Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the tax rule
|
||||
name multi-lingual string The tax rules' name
|
||||
rate decimal (string) Tax rate in percent
|
||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||
the specified product price
|
||||
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
|
||||
home_country string Merchant country (required for reverse charge), can be
|
||||
``null`` or empty string
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
This resource has been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/taxrules/
|
||||
|
||||
Returns a list of all tax rules configured for an event.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/taxrules/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
:query page: The page number in case of a multi-page result set, default is 1
|
||||
:param organizer: The ``slug`` field of a valid organizer
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/taxrules/(id)/
|
||||
|
||||
Returns information on one tax rule, identified by its ID.
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/v1/organizers/bigevents/events/sampleconf/taxrules/1/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: text/javascript
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"name": {"en": "VAT"},
|
||||
"rate": "19.00",
|
||||
"price_includes_tax": true,
|
||||
"eu_reverse_charge": false,
|
||||
"home_country": "DE"
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
:param event: The ``slug`` field of the event to fetch
|
||||
:param id: The ``slug`` field of the sub-event to fetch
|
||||
:statuscode 200: no error
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view it.
|
||||
@@ -40,6 +40,7 @@ quota integer An ID of a quot
|
||||
for all items without restriction.
|
||||
tag string A string that is used for grouping vouchers
|
||||
comment string An internal comment on the voucher
|
||||
subevent integer ID of the date inside an event series this voucher belongs to (or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -85,7 +86,8 @@ Endpoints
|
||||
"variation": null,
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -107,6 +109,7 @@ Endpoints
|
||||
:query integer variation: If set, only vouchers attached to the variation with the given ID will be shown.
|
||||
:query integer quota: If set, only vouchers attached to the quota with the given ID will be shown.
|
||||
:query string tag: If set, only vouchers with the given tag will be shown.
|
||||
:query integer subevent: Only return vouchers of the sub-event with the given ID
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``code``,
|
||||
``max_usages``, ``valid_until``, and ``value``. Default: ``id``
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -149,7 +152,8 @@ Endpoints
|
||||
"variation": null,
|
||||
"quota": null,
|
||||
"tag": "testvoucher",
|
||||
"comment": ""
|
||||
"comment": "",
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
|
||||
@@ -23,6 +23,7 @@ item integer An ID of an ite
|
||||
variation integer An ID of a variation the user is waiting to be
|
||||
available again (or ``null``)
|
||||
locale string Locale of the waiting user
|
||||
subevent integer ID of the date inside an event series this entry belongs to (or ``null``).
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -61,7 +62,8 @@ Endpoints
|
||||
"voucher": null,
|
||||
"item": 2,
|
||||
"variation": null,
|
||||
"locale": "en"
|
||||
"locale": "en",
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -73,6 +75,7 @@ Endpoints
|
||||
have not been sent a voucher.
|
||||
:query integer item: If set, only entries of users waiting for the item with the given ID will be shown.
|
||||
:query integer variation: If set, only entries of users waiting for the variation with the given ID will be shown.
|
||||
:query integer subevent: Only return entries of the sub-event with the given ID
|
||||
:query string ordering: Manually set the ordering of results. Valid fields to be used are ``id``, ``created``,
|
||||
``email``, ``item``. Default: ``created``
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -108,7 +111,8 @@ Endpoints
|
||||
"voucher": null,
|
||||
"item": 2,
|
||||
"variation": null,
|
||||
"locale": "en"
|
||||
"locale": "en",
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
|
||||
11
doc/conf.py
@@ -13,6 +13,10 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
|
||||
from sphinx.util import compat
|
||||
compat.make_admonition = BaseAdmonition # See https://github.com/spinus/sphinxcontrib-images/issues/41
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
@@ -38,9 +42,9 @@ django.setup()
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinxcontrib.httpdomain',
|
||||
'sphinxcontrib.images',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
@@ -281,3 +285,8 @@ texinfo_documents = [
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
|
||||
|
||||
images_config = {
|
||||
'default_image_width': '250px'
|
||||
}
|
||||
|
||||
@@ -60,7 +60,85 @@ your views::
|
||||
def admin_view(request, organizer, event):
|
||||
...
|
||||
|
||||
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``.
|
||||
Similarly, there is ``organizer_permission_required`` and ``OrganizerPermissionRequiredMixin``. In case of
|
||||
event-related views, there is also a signal that allows you to add the view to the event navigation like this::
|
||||
|
||||
|
||||
from django.core.urlresolvers import resolve, reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pretix.control.signals import nav_event
|
||||
|
||||
|
||||
@receiver(nav_event, dispatch_uid='friends_tickets_nav')
|
||||
def navbar_info(sender, request, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
|
||||
return []
|
||||
return [{
|
||||
'label': _('My plugin view'),
|
||||
'icon': 'heart',
|
||||
'url': reverse('plugins:myplugin:index', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'review',
|
||||
}]
|
||||
|
||||
|
||||
Event settings view
|
||||
-------------------
|
||||
|
||||
A special case of a control panel view is a view hooked into the event settings page. For this case, there is a
|
||||
special navigation signal::
|
||||
|
||||
@receiver(nav_event_settings, dispatch_uid='friends_tickets_nav_settings')
|
||||
def navbar_settings(sender, request, **kwargs):
|
||||
url = resolve(request.path_info)
|
||||
return [{
|
||||
'label': _('My settings'),
|
||||
'url': reverse('plugins:myplugin:settings', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
'active': url.namespace == 'plugins:myplugin' and url.url_name == 'settings',
|
||||
}]
|
||||
|
||||
Also, your view should inherit from ``EventSettingsViewMixin`` and your template from ``pretixcontrol/event/settings_base.html``
|
||||
for good integration. If you just want to display a form, you could do it like the following::
|
||||
|
||||
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
|
||||
model = Event
|
||||
permission = 'can_change_settings'
|
||||
form_class = MySettingsForm
|
||||
template_name = 'my_plugin/settings.html'
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse('plugins:myplugin:settings', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
With this template::
|
||||
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %} {% trans "Friends Tickets Settings" %} {% endblock %}
|
||||
{% block inside %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Friends Tickets Settings" %}</legend>
|
||||
{% bootstrap_form form layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
Frontend views
|
||||
--------------
|
||||
@@ -68,35 +146,34 @@ Frontend views
|
||||
Including a custom view into the participant-facing frontend is a little bit different as there is
|
||||
no path prefix like ``control/``.
|
||||
|
||||
First, define your URL in your ``urls.py``, but this time in the ``event_patterns`` section::
|
||||
First, define your URL in your ``urls.py``, but this time in the ``event_patterns`` section and wrapped by
|
||||
``event_url``::
|
||||
|
||||
from django.conf.urls import url
|
||||
from pretix.multidomain import event_url
|
||||
|
||||
from . import views
|
||||
|
||||
event_patterns = [
|
||||
url(r'^mypluginname/', views.frontend_view, name='frontend'),
|
||||
event_url(r'^mypluginname/', views.frontend_view, name='frontend'),
|
||||
]
|
||||
|
||||
You can then implement a view as you would normally do, but you need to apply a decorator to your
|
||||
view if you want pretix's default behavior::
|
||||
|
||||
from pretix.presale.utils import event_view
|
||||
|
||||
@event_view
|
||||
def some_event_view(request, *args, **kwargs):
|
||||
...
|
||||
|
||||
This decorator will check the URL arguments for their ``event`` and ``organizer`` parameters and
|
||||
correctly ensure that:
|
||||
You can then implement a view as you would normally do. It will be automatically ensured that:
|
||||
|
||||
* The requested event exists
|
||||
* The requested event is activated (can be overridden by decorating with ``@event_view(require_live=False)``)
|
||||
* The requested event is active (you can disable this check using ``event_url(…, require_live=True)``)
|
||||
* The event is accessed via the domain it should be accessed
|
||||
* The ``request.event`` attribute contains the correct ``Event`` object
|
||||
* The ``request.organizer`` attribute contains the correct ``Organizer`` object
|
||||
* Your plugin is enabled
|
||||
* The locale is set correctly
|
||||
|
||||
.. versionchanged:: 1.7
|
||||
|
||||
The ``event_url()`` wrapper has been added in 1.7 to replace the former ``@event_view`` decorator. The
|
||||
``event_url()`` wrapper is optional and using ``url()`` still works, but you will not be able to set the
|
||||
``require_live`` setting any more via the decorator. The ``@event_view`` decorator is now deprecated and
|
||||
does nothing.
|
||||
|
||||
REST API viewsets
|
||||
-----------------
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@ Order events
|
||||
There are multiple signals that will be sent out in the ordering cycle:
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: validate_cart, order_paid, order_placed
|
||||
:members: validate_cart, order_fee_calculation, order_paid, order_placed, order_fee_type_name, allow_ticket_download
|
||||
|
||||
Frontend
|
||||
--------
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, contact_form_fields, checkout_confirm_messages
|
||||
:members: html_head, html_footer, footer_links, front_page_top, front_page_bottom, fee_calculation_for_cart, contact_form_fields, question_form_fields, checkout_confirm_messages, checkout_confirm_page_content
|
||||
|
||||
|
||||
.. automodule:: pretix.presale.signals
|
||||
@@ -47,20 +47,26 @@ Backend
|
||||
-------
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer
|
||||
:members: nav_event, html_head, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings, order_info
|
||||
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:members: logentry_display, requiredaction_display
|
||||
:members: logentry_display, logentry_object_link, requiredaction_display
|
||||
|
||||
Vouchers
|
||||
""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: voucher_form_class, voucher_form_html
|
||||
:members: voucher_form_class, voucher_form_html, voucher_form_validation
|
||||
|
||||
Dashboards
|
||||
""""""""""
|
||||
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: event_dashboard_widgets, user_dashboard_widgets
|
||||
|
||||
Ticket designs
|
||||
""""""""""""""
|
||||
|
||||
.. automodule:: pretix.plugins.ticketoutputpdf.signals
|
||||
:members: layout_text_variables
|
||||
|
||||
@@ -10,5 +10,6 @@ Contents:
|
||||
exporter
|
||||
ticketoutput
|
||||
payment
|
||||
invoice
|
||||
customview
|
||||
general
|
||||
|
||||
95
doc/development/api/invoice.rst
Normal file
@@ -0,0 +1,95 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Writing an invoice renderer plugin
|
||||
==================================
|
||||
|
||||
An invoice renderer controls how invoice files are built.
|
||||
The creation of such a plugin is very similar to creating an export output.
|
||||
|
||||
Please read :ref:`Creating a plugin <pluginsetup>` first, if you haven't already.
|
||||
|
||||
Output registration
|
||||
-------------------
|
||||
|
||||
The invoice renderer API does not make a lot of usage from signals, however, it
|
||||
does use a signal to get a list of all available ticket outputs. Your plugin
|
||||
should listen for this signal and return the subclass of ``pretix.base.invoice.BaseInvoiceRenderer``
|
||||
that we'll provide in this plugin::
|
||||
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
|
||||
|
||||
@receiver(register_invoice_renderers, dispatch_uid="output_custom")
|
||||
def register_infoice_renderers(sender, **kwargs):
|
||||
from .invoice import MyInvoiceRenderer
|
||||
return MyInvoiceRenderer
|
||||
|
||||
|
||||
The renderer class
|
||||
------------------
|
||||
|
||||
.. class:: pretix.base.invoice.BaseInvoiceRenderer
|
||||
|
||||
The central object of each invoice renderer is the subclass of ``BaseInvoiceRenderer``.
|
||||
|
||||
.. py:attribute:: BaseInvoiceRenderer.event
|
||||
|
||||
The default constructor sets this property to the event we are currently
|
||||
working for.
|
||||
|
||||
.. autoattribute:: identifier
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. autoattribute:: verbose_name
|
||||
|
||||
This is an abstract attribute, you **must** override this!
|
||||
|
||||
.. automethod:: generate
|
||||
|
||||
Helper class for reportlab-base renderers
|
||||
-----------------------------------------
|
||||
|
||||
All PDF rendering that ships with pretix is based on reportlab. We recommend to read the
|
||||
`reportlab User Guide`_ to understand all the concepts used here.
|
||||
|
||||
If you want to implement a renderer that also uses report lab, this helper class might be
|
||||
convenient to you:
|
||||
|
||||
|
||||
.. class:: pretix.base.invoice.BaseReportlabInvoiceRenderer
|
||||
|
||||
.. py:attribute:: BaseReportlabInvoiceRenderer.pagesize
|
||||
|
||||
.. py:attribute:: BaseReportlabInvoiceRenderer.left_margin
|
||||
|
||||
.. py:attribute:: BaseReportlabInvoiceRenderer.right_margin
|
||||
|
||||
.. py:attribute:: BaseReportlabInvoiceRenderer.top_margin
|
||||
|
||||
.. py:attribute:: BaseReportlabInvoiceRenderer.bottom_margin
|
||||
|
||||
.. py:attribute:: BaseReportlabInvoiceRenderer.doc_template_class
|
||||
|
||||
.. py:attribute:: BaseReportlabInvoiceRenderer.invoice
|
||||
|
||||
.. automethod:: _init
|
||||
|
||||
.. automethod:: _get_stylesheet
|
||||
|
||||
.. automethod:: _register_fonts
|
||||
|
||||
.. automethod:: _on_first_page
|
||||
|
||||
.. automethod:: _on_other_page
|
||||
|
||||
.. automethod:: _get_first_page_frames
|
||||
|
||||
.. automethod:: _get_other_page_frames
|
||||
|
||||
.. automethod:: _build_doc
|
||||
|
||||
.. _reportlab User Guide: https://www.reportlab.com/docs/reportlab-userguide.pdf
|
||||
@@ -114,6 +114,19 @@ method to make your receivers available::
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
You can optionally specify code that is executed when your plugin is activated for an event
|
||||
in the ``installed`` method::
|
||||
|
||||
class PaypalApp(AppConfig):
|
||||
…
|
||||
|
||||
def installed(self, event):
|
||||
pass # Your code here
|
||||
|
||||
|
||||
Note that ``installed`` will *not* be called if the plugin in indirectly activated for an event
|
||||
because the event is created with settings copied from another event.
|
||||
|
||||
Views
|
||||
-----
|
||||
|
||||
|
||||
@@ -58,6 +58,8 @@ The output class
|
||||
|
||||
.. autoattribute:: is_enabled
|
||||
|
||||
.. autoattribute:: multi_download_enabled
|
||||
|
||||
.. autoattribute:: settings_form_fields
|
||||
|
||||
.. automethod:: settings_content_render
|
||||
|
||||
@@ -59,7 +59,7 @@ If an item is assigned to multiple quotas, it can only be bought if *all of them
|
||||
If multiple items are assigned to the same quota, the quota will be counted as sold out as soon as the
|
||||
*sum* of the two items exceeds the quota limit.
|
||||
|
||||
The availability of a quota is currently calculated by substracting the following numbers from the quota
|
||||
The availability of a quota is currently calculated by subtracting the following numbers from the quota
|
||||
limit:
|
||||
|
||||
* The number of orders placed for an item that are either already paid or within their granted payment period
|
||||
|
||||
@@ -14,7 +14,7 @@ Implementing a task
|
||||
A common pattern for implementing asynchronous tasks can be seen a lot in ``pretix.base.services``
|
||||
and looks like this::
|
||||
|
||||
from pretix.celery import app
|
||||
from pretix.celery_app import app
|
||||
|
||||
@app.task
|
||||
def my_task(argument1, argument2):
|
||||
|
||||
@@ -2,6 +2,9 @@ Sending Email
|
||||
=============
|
||||
|
||||
pretix allows event organizers to configure how they want to send emails to their users in multiple ways.
|
||||
Therefore, all emails should be sent through the following function:
|
||||
Therefore, all emails should be sent through the following function.
|
||||
|
||||
If the email you send is related to an order, you should also take a look at the
|
||||
:py:meth:`~pretix.base.models.Order.send_mail` of the order model.
|
||||
|
||||
.. autofunction:: pretix.base.services.mail.mail
|
||||
|
||||
@@ -21,7 +21,10 @@ Organizers and events
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.Event
|
||||
:members:
|
||||
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running, get_cache, lock, get_plugins, get_mail_backend, payment_term_last, get_payment_providers, get_invoice_renderers, active_subevents, invoice_renderer, settings
|
||||
|
||||
.. autoclass:: pretix.base.models.SubEvent
|
||||
:members: get_date_from_display, get_time_from_display, get_date_to_display, get_date_range_display, presale_has_ended, presale_is_running
|
||||
|
||||
.. autoclass:: pretix.base.models.Team
|
||||
:members:
|
||||
@@ -29,6 +32,15 @@ Organizers and events
|
||||
.. autoclass:: pretix.base.models.RequiredAction
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.EventMetaProperty
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.EventMetaValue
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.SubEventMetaValue
|
||||
:members:
|
||||
|
||||
|
||||
Items
|
||||
-----
|
||||
@@ -42,6 +54,15 @@ Items
|
||||
.. autoclass:: pretix.base.models.ItemVariation
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.SubEventItem
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.SubEventItemVariation
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.ItemAddOn
|
||||
:members:
|
||||
|
||||
.. autoclass:: pretix.base.models.Question
|
||||
:members:
|
||||
|
||||
|
||||
@@ -10,6 +10,3 @@ Developer documentation
|
||||
implementation/index
|
||||
api/index
|
||||
structure
|
||||
|
||||
.. TODO::
|
||||
Document settings objects, ItemVariation objects, form fields.
|
||||
|
||||
@@ -20,7 +20,6 @@ Your should install the following on your system:
|
||||
|
||||
* Python 3.4 or newer
|
||||
* ``pip`` for Python 3 (Debian package: ``python3-pip``)
|
||||
* ``pyvenv`` for Python 3 (Debian package: ``python3-venv``)
|
||||
* ``python-dev`` for Python 3 (Debian package: ``python3-dev``)
|
||||
* ``libffi`` (Debian package: ``libffi-dev``)
|
||||
* ``libssl`` (Debian package: ``libssl-dev``)
|
||||
@@ -37,7 +36,7 @@ Please execute ``python -V`` or ``python3 -V`` to make sure you have Python 3.4
|
||||
execute ``pip3 -V`` to check. Then use Python's internal tools to create a virtual
|
||||
environment and activate it for your current session::
|
||||
|
||||
pyvenv env
|
||||
python3 -m venv env
|
||||
source env/bin/activate
|
||||
|
||||
You should now see a ``(env)`` prepended to your shell prompt. You have to do this
|
||||
|
||||
@@ -19,10 +19,12 @@ same team. We update them regularly to make them compatible with the latest
|
||||
pretix releases:
|
||||
|
||||
* `SEPA direct debit`_
|
||||
* `Wirecard payment`_
|
||||
* `Pages`_
|
||||
* `Passbook/Wallet ticket output`_
|
||||
* `Cartshare`_
|
||||
* `Fontpack Free fonts`_
|
||||
* `Mailing list subscription`_
|
||||
|
||||
The following closed-source plugins are available to customers of the hosted pretix.eu platform.
|
||||
Please get in touch with the pretix team if you want to have them for your self-hosted
|
||||
@@ -34,10 +36,12 @@ pretix installation:
|
||||
* Integration with MailChimp
|
||||
|
||||
The following plugins are from independent third-party authors, so we can make
|
||||
no statements about their stability or compatibility:
|
||||
no statements about their functionality, security, stability or compatibility:
|
||||
|
||||
* `esPass ticket output`_
|
||||
* `IcePay integration`_
|
||||
* `Average price chart`_
|
||||
* `Pay in cash upon arrival`_
|
||||
|
||||
.. _SEPA direct debit: https://github.com/pretix/pretix-sepadebit
|
||||
.. _Passbook/Wallet ticket output: https://github.com/pretix/pretix-passbook
|
||||
@@ -46,3 +50,7 @@ no statements about their stability or compatibility:
|
||||
.. _esPass ticket output: https://github.com/esPass/pretix-espass
|
||||
.. _IcePay integration: https://github.com/chotee/pretix-icepay
|
||||
.. _Fontpack Free fonts: https://github.com/pretix/pretix-fontpack-free
|
||||
.. _Wirecard payment: https://github.com/pretix/pretix-wirecard
|
||||
.. _Mailing list subscription: https://github.com/pretix/pretix-newsletter-ml
|
||||
.. _Average price chart: https://github.com/rixx/pretix-avgchart
|
||||
.. _Pay in cash upon arrival: https://github.com/pc-coholic/pretix-cashpayment
|
||||
|
||||
@@ -99,6 +99,7 @@ uses to communicate with the pretix server.
|
||||
"variation": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
@@ -107,10 +108,10 @@ uses to communicate with the pretix server.
|
||||
}
|
||||
|
||||
:query query: Search query
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
:query key: Secret API key
|
||||
:statuscode 200: Valid request
|
||||
:statuscode 404: Unknown organizer or event
|
||||
:statuscode 403: Invalid authorization key
|
||||
|
||||
.. http:get:: /pretixdroid/api/(organizer)/(event)/download/
|
||||
|
||||
@@ -140,6 +141,7 @@ uses to communicate with the pretix server.
|
||||
"variation": null,
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
sphinx
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
|
||||
BIN
doc/screens/event/create_step1.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
doc/screens/event/create_step2.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
doc/screens/event/create_step3.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
doc/screens/event/create_step4.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
doc/screens/event/tax_detail.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
doc/screens/event/tax_list.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
doc/screens/organizer/edit.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
doc/screens/organizer/event_list.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
doc/screens/organizer/list.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
doc/screens/organizer/team_detail.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
doc/screens/organizer/team_edit.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
doc/screens/organizer/team_list.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
97
doc/user/events/create.rst
Normal file
@@ -0,0 +1,97 @@
|
||||
Creating an event
|
||||
=================
|
||||
|
||||
After you have created an organizer account, the next step is to create your event. An event is the basic object in
|
||||
pretix that everything is organized around. One event corresponds to one ticket shop with all its products, quotas,
|
||||
orders and settings.
|
||||
|
||||
To create an event, you can click the "Create a new event" tile on your dashboard or the button above the list of
|
||||
events. You will then be presented with the first step of event creation:
|
||||
|
||||
.. thumbnail:: ../../screens/event/create_step1.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Here, you first need to decide for the organizer the event belongs to. You will not be able to change this
|
||||
association later. This will determine default settings for the event, as well as access control to the event's
|
||||
settings.
|
||||
|
||||
Second, you need to select the languages that the ticket shop should be available in. You can change this setting
|
||||
later, but if you select it correctly now, it will automatically ask you for all descriptions in the respective
|
||||
languages starting from the next step.
|
||||
|
||||
Last on this page, you can decide if this event represents an event series. In this cases, the event will turn into
|
||||
multiple events included in once, meaning that you will get one combined ticket shop for multiple actual events. This
|
||||
is useful if you have a large number of events that are very similar to each other and that should be sold together
|
||||
(i.e. users should be able to buy tickets for multiple events at the same time). Those single events can differ in
|
||||
available products, quotas, prices and some meta information, but most settings need to be the same for all of them.
|
||||
We recommend to use this feature only if you really know that you need it and if you really run a lot of events, not if
|
||||
you run e.g. a yearly conference.
|
||||
|
||||
Once you set these values, you can procede to the next step:
|
||||
|
||||
.. thumbnail:: ../../screens/event/create_step2.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
In this step, you will be asked more detailled questions about your event. In particular, you can fill in the
|
||||
following fields:
|
||||
|
||||
Name
|
||||
This is the public name of your event. It should be descriptive and tell both you and the user which event you are
|
||||
dealing with, but should still be concise. You probably know how your event is named already ;)
|
||||
|
||||
Short form
|
||||
This will be used in multiple places. For example, the URL of your ticket shop will include this short form of
|
||||
your event name, but it will also be the default prefix e.g. for invoice numbers. We recommend to use some natural
|
||||
abbreviation of your event name, maybe together with a date, of no more than 10 characters. This is the only value
|
||||
on this page that can't be changed later.
|
||||
|
||||
Event start time
|
||||
The date and time that your event starts at. You can later configure settings to hide the time, if you don't want
|
||||
to show that.
|
||||
|
||||
Event end time
|
||||
The date and time your event ends at. You can later configure settings to hide this value completely -- or you can
|
||||
just leave it empty. It's optional!
|
||||
|
||||
Location
|
||||
This is the location of your event in a human-readable format. We will show this on the ticket shop frontpage, but
|
||||
it might also be used e.g. in Wallet tickets.
|
||||
|
||||
Event currency
|
||||
This is the currency all prices and payments in your shop will be handled in.
|
||||
|
||||
Sales tax rate
|
||||
If you need to pay a form of sales tax (also known as VAT in many countries) on your products, you can set a tax rate
|
||||
in percent here that will be used as a default later. After creating your event, you can also create multiple tax
|
||||
rates or fine-tune the tax settings.
|
||||
|
||||
Default language
|
||||
If you selected multiple supported languages in the previous step, you can now decide which one should be
|
||||
displayed by default.
|
||||
|
||||
Start of presale
|
||||
If you set this date, no ticket will be sold before this date. We normally recommend not to set this date during
|
||||
event creation because it will make testing your shop harder.
|
||||
|
||||
End of presale
|
||||
If you set this date, no ticket will be sold after this date.
|
||||
|
||||
If all of this is set, you can proceed to the next step. If this is your first event, there will not be a next step
|
||||
and you are done! If you have already created events before, you will be asked if you want to copy settings from one
|
||||
of them:
|
||||
|
||||
.. thumbnail:: ../../screens/event/create_step3.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
If you do so, all products, categories, quotas and most settings of the other event will be taken over. You should
|
||||
still review them if they make sense for your new event, but it could save you a lot of work. After this step, your
|
||||
event is created successfully:
|
||||
|
||||
.. thumbnail:: ../../screens/event/create_step4.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
You can now fine-tune all settings to your liking, publish your event and start selling tickets!
|
||||
109
doc/user/events/taxes.rst
Normal file
@@ -0,0 +1,109 @@
|
||||
Tax rules
|
||||
=========
|
||||
|
||||
In most countries, you will be required to pay some form of sales tax for your event tickets. If you don't know about
|
||||
the exact rules, you should consult a professional tax consultant right now.
|
||||
|
||||
To implement those taxes in pretix, you can create one or multiple "tax rules". A tax rule specifies when and at what
|
||||
rate should be calculated on a product price. Taxes will then be correctly displayed in the product list, order
|
||||
details and on invoices.
|
||||
|
||||
At the time of this writing, every product can be assigned exactly one tax rule. To view and change the tax rules of
|
||||
your event, go to the respective section in your event's settings:
|
||||
|
||||
.. thumbnail:: ../../screens/event/tax_list.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
On this page, you can create, edit and delete your tax rules. Clicking on the name of a tax rule will take you to its
|
||||
detailled settings:
|
||||
|
||||
.. thumbnail:: ../../screens/event/tax_detail.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
Here, you can tune the following parameters:
|
||||
|
||||
Name
|
||||
What is the (short) name of this tax? This is probably "VAT" in English and should be very short as it will be
|
||||
displayed in lots of places.
|
||||
|
||||
Rate
|
||||
This is the tax rate in percent.
|
||||
|
||||
The configured product prices include the tax amount
|
||||
If this setting is enabled (the default), then a product configured to a price of 10.00 EUR will, at a tax rate of
|
||||
19.00 %, be interpreted as a product with a total gross price of 10.00 EUR including 1.60 EUR taxes, leading to a
|
||||
net price of 8.40 EUR. If you disable this setting, the price will be interpreted as a net price of 10.00 EUR,
|
||||
leading to a total price to pay of 11.90 EUR.
|
||||
|
||||
Use EU reverse charge taxation rules
|
||||
This enables reverse charge taxation (see section below).
|
||||
|
||||
Merchant country
|
||||
This is probably your country of residence, but in some cases it could also be the country your event is
|
||||
located in. This is the place of taxation in the sense of EU reverse charge rules (see section below).
|
||||
|
||||
EU reverse charge
|
||||
-----------------
|
||||
|
||||
.. warning:: Everything contained in this section is not legal advice. Please consult a tax consultant
|
||||
before making decisions. We are not responsible for the correct handling of taxes in your
|
||||
ticket shop.
|
||||
|
||||
"Reverse charge" is a rule in European VAT legislation that specifies how taxes are paid
|
||||
if you provide goods to a buyer in a different European country than you reside in yourself.
|
||||
If the buyer is a VAT-paying business in their country, you charge them only the net price without
|
||||
taxes and state that the buyer is responsible for paying the correct taxes themselves.
|
||||
|
||||
.. warning:: We firmly believe that reverse charge rules are **not applicable** for most events handled
|
||||
with pretix and therefore **strongly recommend not to enable this feature** if you do not have
|
||||
a specific reason to do so. The reasoning behind this is that according to article 52 of the
|
||||
`VAT directive`_ (page 17), the place of supply is always the location of your event and
|
||||
therefore the tax rate of the event country always has to be paid regardless of the location
|
||||
of the visitor.
|
||||
|
||||
If you enable the reverse charge feature and specify your merchant country, then the following process
|
||||
will be performed during order creation:
|
||||
|
||||
* The user will first be presented with the "normal" prices (net or gross, as configured).
|
||||
|
||||
* The user adds a product to their cart. The cart will at this point always show gross prices *with*
|
||||
taxes.
|
||||
|
||||
* In the next step, the user can enter an invoice address. Tax will be removed from the price if one of the
|
||||
following statements is true:
|
||||
|
||||
* The invoice address is in a non-EU country.
|
||||
|
||||
* The invoice address is a business address in an EU-country different from the merchant country and has a valid VAT ID.
|
||||
|
||||
* In the second case, a reverse charge note will be added to the invoice.
|
||||
|
||||
VAT IDs are validated against the EUs validation web service. Should that service be unavailable, the user
|
||||
needs to pay VAT tax and reclaim the taxes at a later point in time with their government.
|
||||
|
||||
If you and the buyer are residing in EU countries that use different currencies, the invoice will show
|
||||
the total and VAT amount also in the local currency of the buyer, if the system was able to obtain a
|
||||
conversion rate from the European Central Bank's webservice within the last 7 days.
|
||||
|
||||
For existing orders, a change of the invoice address will not result in a change of taxes automatically.
|
||||
You can trigger this manually in the backend by going to the order's detail view. There, first click
|
||||
the "Check" button next to the VAT ID. Then, go to "Change products" and select the option "Recalculate
|
||||
taxes" at the end of the page.
|
||||
|
||||
.. note:: In the invoicing settings, you should turn the setting "Ask for VAT ID" on for this to work.
|
||||
|
||||
.. note:: During back-and-forth modification of taxation status, unfortunately there can be rounding
|
||||
errors of usually up to one cent from the intended price. This is unavoidable due to the
|
||||
flexible nature in which prices are being calculated.
|
||||
|
||||
Taxation of payment fees
|
||||
------------------------
|
||||
|
||||
In the payment part of your event settings, you can choose the tax rule that needs to be applied for
|
||||
payment method fees. This works in the same way as product prices, with the small difference that the
|
||||
"configured product prices include the tax amount" settings is ignored and payment fees will always be
|
||||
treated as gross values.
|
||||
|
||||
.. _VAT directive: http://eur-lex.europa.eu/legal-content/EN/TXT/PDF/?uri=CELEX:32006L0112&from=EN
|
||||
@@ -1,7 +1,13 @@
|
||||
User Guide
|
||||
==========
|
||||
|
||||
This section of our documentation is dedicated to show you the way around pretix if you are an event organizer
|
||||
wanting to use pretix to sell tickets.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
organizers/index
|
||||
events/create
|
||||
events/taxes
|
||||
payments/index
|
||||
|
||||
112
doc/user/organizers/index.rst
Normal file
@@ -0,0 +1,112 @@
|
||||
Organizer accounts and teams
|
||||
============================
|
||||
|
||||
Organizer account
|
||||
-----------------
|
||||
|
||||
The basis of all your operations within pretix is your organizer account. It represents an entity that is running
|
||||
events, for example a company, yourself or any other institution.
|
||||
Every event belongs to one organizer account and events within the same organizer account are assumed to belong together
|
||||
in some sense, whereas events in different organizer accounts are completely isolated.
|
||||
|
||||
If you want to use the hosted pretix service, you can create an organizer account on our `Get started`_ page. Otherwise,
|
||||
ask your pretix administrator for access to an organizer account.
|
||||
|
||||
You can find out all organizer accounts you have access to by going to your global dashboard (click on the pretix logo
|
||||
in the top-left corner) and then select "Organizers" from the navigation bar on the left side. Then, choose one of the
|
||||
organizer accounts presented, if there are multiple of them:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/list.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
This overview shows you all event that belong to the organizer and you have access to:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/event_list.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
With the "Edit" button at the top, next to the organizer account name, you can modify properties of the organizer
|
||||
account such as its name and display settings for the public profile page of the organizer account:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/edit.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
.. tip::
|
||||
|
||||
The profile page will be shown as ``https://pretix.eu/slug/`` where ``slug`` is to be replaced by the short form of
|
||||
the organizer name that you entered during account creation and ``pretix.eu`` is to be replaced by your
|
||||
installation's domain name if you are not using our hosted service.
|
||||
|
||||
Instead, you can also use a custom domain for the profile page and your events, for example
|
||||
``https://tickets.example.com/`` if ``example.com`` is a domain that you own. In this case, please contact the pretix
|
||||
hosted support or your system administrator to set up the custom domain.
|
||||
|
||||
Teams
|
||||
-----
|
||||
|
||||
We don't expect you to work on your events all by yourself and therefore, pretix comes with ways to invite your fellow
|
||||
team members to access your pretix organizer account. To manage teams, click on the "Teams" link on your organizer
|
||||
settings page (see above how to find it). This shows you a list of teams that should contain at least one team already:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/team_list.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
If you click on a team name, you get to a page that shows you the current members of the team:
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/team_detail.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
You see that there is a list of pretix user accounts (i.e. email addresses), who are part of the team. To add a user to
|
||||
the team, just enter their email address in the text box next to the "Add" button. If the user already has an account
|
||||
in the pretix system they will instantly get access to the team. Otherwise, they will be sent an email with an invitation
|
||||
link that can be used to create an account. This account will then instantly have access to the team. Users can be part
|
||||
of as many teams as you want.
|
||||
|
||||
In the section below, you can also create access tokens for our :ref:`rest-api`. You can read more on this topic in the
|
||||
section :ref:`rest-auth` of the API documentation.
|
||||
|
||||
Next to the team name, you again see a button called "Edit" that allows you to modify the permissions of the team.
|
||||
Permissions separate into two areas:
|
||||
|
||||
* **Organizer permissions** allow actions on the level of an organizer account, in particular:
|
||||
|
||||
* Can create events – To create a new event under this organizer account, users need to have this permission
|
||||
|
||||
* Can change teams and permissions – This permission is required to perform the kind of action you are doing right now.
|
||||
Anyone with this permission can assign arbitrary other permissions to themselves, so this is the most powerful
|
||||
permission there is to give.
|
||||
|
||||
* Can change organizer settings – This permission is required to perform changes to the settings of the organizer
|
||||
account, e.g. its name or display settings.
|
||||
|
||||
* **Event permissions** allow actions on the level of an event. You can give the team access to all events of the
|
||||
organizer (including future ones that are not yet created) or just a selected set of events. The specific permissions to choose from are:
|
||||
|
||||
* Can change event settings – This permission gives access to most areas of the control panel that are not controlled
|
||||
by one of the other event permissions, especially those that are related to setting up and configuring the event.
|
||||
|
||||
* Can change product settings – This permission allows to create and modify products and objects that are closely
|
||||
related to products, such as product categories, quotas, and questions.
|
||||
|
||||
* Can view orders – This permission allows viewing the list of orders and allindividual order details, but not
|
||||
changing anything about it. This also includes the various exports offered.
|
||||
|
||||
* Can change orders – This permission allows all actions that involve changing an order, such as changing the products
|
||||
in an order, marking an order as paid or refunden, importing banking data, etc. This only works properly if the
|
||||
same users also have the "Can view orders" permission.
|
||||
|
||||
* Can view vouchers – This permission allows viewing the list of vouchers including the voucher codes themselves and
|
||||
their redemption status.
|
||||
|
||||
* Can change vouchers – This permission allows to create and modify vouchers in all their details. It only works
|
||||
properly if the same users also have the "Can view vouchers" permission.
|
||||
|
||||
.. thumbnail:: ../../screens/organizer/team_edit.png
|
||||
:align: center
|
||||
:class: screenshot
|
||||
|
||||
.. _Get started: https://pretix.eu/about/en/setup
|
||||
@@ -3,7 +3,7 @@
|
||||
Bank transfer
|
||||
=============
|
||||
|
||||
To accept payments with bank transfer, you only need to fill one important field in pretix' settings: In "Bank
|
||||
To accept payments with bank transfer, you only need to fill out one important field in pretix' settings: In "Bank
|
||||
account details" you should specify everything one needs to know to transfer money to you, e.g. your IBAN and BIC,
|
||||
the name of your bank and for international transfers, preferably also your address and the bank's address.
|
||||
|
||||
@@ -17,6 +17,7 @@ The easiest way to import payment data is to download a CSV file from your onlin
|
||||
export of some sort. You can go to "Import bank data" in pretix to upload a new file:
|
||||
|
||||
.. image:: img/bank1.png
|
||||
:class: screenshot
|
||||
|
||||
If you upload a file for the first time, pretix will not know what information is contained in which column as every
|
||||
bank builds completely different CSV files. Therefore, pretix will ask you for that information. It will show you the
|
||||
|
||||
@@ -8,41 +8,49 @@ PayPal account, you can create one on `paypal.com`_.
|
||||
If you look into pretix' settings, you are required to fill in two keys:
|
||||
|
||||
.. image:: img/paypal_pretix.png
|
||||
:class: screenshot
|
||||
|
||||
Unfortunately, it is not straightforward how to get those keys from PayPal's website. In order to do so, you
|
||||
need to go to `developer.paypal.com`_ to link the account to your pretix event.
|
||||
Click on "Log In" in the top-right corner and log in with your PayPal account.
|
||||
|
||||
.. image:: img/paypal2.png
|
||||
:class: screenshot
|
||||
|
||||
Then, click on "Dashboard" in the top-right corner.
|
||||
|
||||
.. image:: img/paypal3.png
|
||||
:class: screenshot
|
||||
|
||||
In the dashboard, scroll down until you see the headline "REST API Apps". Click "Create App".
|
||||
|
||||
.. image:: img/paypal4.png
|
||||
:class: screenshot
|
||||
|
||||
Enter any name for the application that helps you to identify it later. Then confirm with "Create App".
|
||||
|
||||
.. image:: img/paypal5.png
|
||||
:class: screenshot
|
||||
|
||||
On the next page, before you do anything else, switch the mode on the right to "Live" to get the correct keys.
|
||||
Then, copy the "Client ID" and the "Secret" and enter them into the appropriate fields in the payment settings in
|
||||
pretix.
|
||||
|
||||
.. image:: img/paypal6.png
|
||||
:class: screenshot
|
||||
|
||||
Finally, we need to create a webhook. The webhook tells PayPal to notify pretix e.g. if a payment gets cancelled so
|
||||
pretix can cancel the ticket as well. If you have multiple events connected to your PayPal account, you need multiple
|
||||
webhooks. To create one, scroll a bit down and click "Add Webhook".
|
||||
|
||||
.. image:: img/paypal7.png
|
||||
:class: screenshot
|
||||
|
||||
Then, enter the webhook URL that you find on the pretix settings page. It should look similar to the one in the
|
||||
screenshot but contain your event name. Tick the box "All events" and save.
|
||||
|
||||
.. image:: img/paypal8.png
|
||||
:class: screenshot
|
||||
|
||||
That's it, you are ready to go!
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ Dashboard. As you can see in the following screenshot, you will be presented wit
|
||||
and one for live payments. In each set, there is a secret and a publishable keys.
|
||||
|
||||
.. image:: img/stripe1.png
|
||||
:class: screenshot
|
||||
|
||||
Choose one of the two sets and copy the two keys to the appropriate fields in pretix' settings. To perform actual
|
||||
payments, you will need to use the live keys, but you can use the test keys to test the payment flow before you go live.
|
||||
@@ -21,6 +22,7 @@ that you are currently on. Then, click "Add endpoint" and enter the URL that you
|
||||
configuration in pretix' settings.
|
||||
|
||||
.. image:: img/stripe2.png
|
||||
:class: screenshot
|
||||
|
||||
Again, you can choose between live mode and test mode here.
|
||||
|
||||
|
||||
@@ -47,14 +47,15 @@ question = Question.objects.create(
|
||||
event=event, question='Age',
|
||||
type=Question.TYPE_NUMBER, required=False
|
||||
)
|
||||
tr19 = event.tax_rules.create(rate=19)
|
||||
item_ticket = Item.objects.create(
|
||||
event=event, category=cat_tickets, name='Ticket',
|
||||
default_price=23, tax_rate=19, admission=True
|
||||
default_price=23, tax_rule=tr19, admission=True
|
||||
)
|
||||
item_ticket.questions.add(question)
|
||||
item_shirt = Item.objects.create(
|
||||
event=event, category=cat_merch, name='T-Shirt',
|
||||
default_price=15, tax_rate=19
|
||||
default_price=15, tax_rule=tr19
|
||||
)
|
||||
var_s = ItemVariation.objects.create(item=item_shirt, value='S')
|
||||
var_m = ItemVariation.objects.create(item=item_shirt, value='M')
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.5.0"
|
||||
__version__ = "1.8.1"
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from pretix.base.models import Event
|
||||
@@ -13,6 +17,18 @@ class EventPermission(BasePermission):
|
||||
return True
|
||||
return False
|
||||
|
||||
if request.user.is_authenticated:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
|
||||
last_used = request.session.get('pretix_auth_last_used', time.time())
|
||||
if time.time() - request.session.get('pretix_auth_login_time', time.time()) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
|
||||
logout(request)
|
||||
request.session['pretix_auth_login_time'] = 0
|
||||
return False
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
|
||||
return False
|
||||
request.session['pretix_auth_last_used'] = int(time.time())
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
|
||||
else request.user)
|
||||
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
||||
|
||||
@@ -1,10 +1,55 @@
|
||||
from django_countries.serializers import CountryFieldMixin
|
||||
from rest_framework.fields import Field
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.models import Event, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.models.items import SubEventItem, SubEventItemVariation
|
||||
|
||||
|
||||
class MetaDataField(Field):
|
||||
|
||||
def to_representation(self, value):
|
||||
return {
|
||||
v.property.name: v.value for v in value.meta_values.all()
|
||||
}
|
||||
|
||||
|
||||
class EventSerializer(I18nAwareModelSerializer):
|
||||
meta_data = MetaDataField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = Event
|
||||
fields = ('name', 'slug', 'live', 'currency', 'date_from',
|
||||
'date_to', 'date_admission', 'is_public', 'presale_start',
|
||||
'presale_end', 'location')
|
||||
'presale_end', 'location', 'has_subevents', 'meta_data')
|
||||
|
||||
|
||||
class SubEventItemSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ('item', 'price')
|
||||
|
||||
|
||||
class SubEventItemVariationSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = SubEventItemVariation
|
||||
fields = ('variation', 'price')
|
||||
|
||||
|
||||
class SubEventSerializer(I18nAwareModelSerializer):
|
||||
item_price_overrides = SubEventItemSerializer(source='subeventitem_set', many=True)
|
||||
variation_price_overrides = SubEventItemVariationSerializer(source='subeventitemvariation_set', many=True)
|
||||
meta_data = MetaDataField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
|
||||
'presale_start', 'presale_end', 'location',
|
||||
'item_price_overrides', 'variation_price_overrides', 'meta_data')
|
||||
|
||||
|
||||
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
@@ -21,17 +23,26 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
|
||||
'position')
|
||||
|
||||
|
||||
class ItemTaxRateField(serializers.Field):
|
||||
def to_representation(self, i):
|
||||
if i.tax_rule:
|
||||
return str(Decimal(i.tax_rule.rate))
|
||||
else:
|
||||
return str(Decimal('0.00'))
|
||||
|
||||
|
||||
class ItemSerializer(I18nAwareModelSerializer):
|
||||
addons = InlineItemAddOnSerializer(many=True)
|
||||
variations = InlineItemVariationSerializer(many=True)
|
||||
tax_rate = ItemTaxRateField(source='*', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ('id', 'category', 'name', 'active', 'description',
|
||||
'default_price', 'free_price', 'tax_rate', 'admission',
|
||||
'default_price', 'free_price', 'tax_rate', 'tax_rule', 'admission',
|
||||
'position', 'picture', 'available_from', 'available_until',
|
||||
'require_voucher', 'hide_without_voucher', 'allow_cancel',
|
||||
'min_per_order', 'max_per_order', 'has_variations',
|
||||
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations',
|
||||
'variations', 'addons')
|
||||
|
||||
|
||||
@@ -61,4 +72,4 @@ class QuotaSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ('id', 'name', 'size', 'items', 'variations')
|
||||
fields = ('id', 'name', 'size', 'items', 'variations', 'subevent')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
@@ -6,13 +8,25 @@ from pretix.base.models import (
|
||||
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
|
||||
QuestionAnswer,
|
||||
)
|
||||
from pretix.base.models.orders import OrderFee
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
|
||||
|
||||
class CompatibleCountryField(serializers.Field):
|
||||
def to_representation(self, instance: InvoiceAddress):
|
||||
if instance.country:
|
||||
return str(instance.country)
|
||||
else:
|
||||
return instance.country_old
|
||||
|
||||
|
||||
class InvoiceAdddressSerializer(I18nAwareModelSerializer):
|
||||
country = CompatibleCountryField(source='*')
|
||||
|
||||
class Meta:
|
||||
model = InvoiceAddress
|
||||
fields = ('last_modified', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id')
|
||||
fields = ('last_modified', 'is_business', 'company', 'name', 'street', 'zipcode', 'city', 'country', 'vat_id',
|
||||
'vat_id_validated')
|
||||
|
||||
|
||||
class AnswerSerializer(I18nAwareModelSerializer):
|
||||
@@ -86,25 +100,48 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email',
|
||||
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'checkins', 'downloads', 'answers')
|
||||
'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads',
|
||||
'answers', 'tax_rule')
|
||||
|
||||
|
||||
class OrderFeeSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = OrderFee
|
||||
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
|
||||
|
||||
|
||||
class PaymentFeeLegacyField(serializers.Field):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.attr = kwargs.pop('attribute')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_representation(self, instance: Order):
|
||||
return str(
|
||||
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
|
||||
Decimal('0.00'))
|
||||
)
|
||||
|
||||
|
||||
class OrderSerializer(I18nAwareModelSerializer):
|
||||
invoice_address = InvoiceAdddressSerializer()
|
||||
positions = OrderPositionSerializer(many=True)
|
||||
fees = OrderFeeSerializer(many=True)
|
||||
downloads = OrderDownloadsField(source='*')
|
||||
payment_fee = PaymentFeeLegacyField(source='*', attribute='value') # TODO: Remove in 1.9
|
||||
payment_fee_tax_rate = PaymentFeeLegacyField(source='*', attribute='tax_rate') # TODO: Remove in 1.9
|
||||
payment_fee_tax_value = PaymentFeeLegacyField(source='*', attribute='tax_value') # TODO: Remove in 1.9
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value',
|
||||
'total', 'comment', 'invoice_address', 'positions', 'downloads')
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value')
|
||||
|
||||
|
||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
class Meta:
|
||||
model = InvoiceLine
|
||||
fields = ('description', 'gross_value', 'tax_value', 'tax_rate')
|
||||
fields = ('description', 'gross_value', 'tax_value', 'tax_rate', 'tax_name')
|
||||
|
||||
|
||||
class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
@@ -114,5 +151,6 @@ class InvoiceSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ('order', 'invoice_no', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
|
||||
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines')
|
||||
fields = ('order', 'number', 'is_cancellation', 'invoice_from', 'invoice_to', 'date', 'refers', 'locale',
|
||||
'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines',
|
||||
'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date')
|
||||
|
||||
@@ -7,4 +7,4 @@ class VoucherSerializer(I18nAwareModelSerializer):
|
||||
model = Voucher
|
||||
fields = ('id', 'code', 'max_usages', 'redeemed', 'valid_until', 'block_quota',
|
||||
'allow_ignore_quota', 'price_mode', 'value', 'item', 'variation', 'quota',
|
||||
'tag', 'comment')
|
||||
'tag', 'comment', 'subevent')
|
||||
|
||||
@@ -6,4 +6,4 @@ class WaitingListSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale')
|
||||
fields = ('id', 'created', 'email', 'voucher', 'item', 'variation', 'locale', 'subevent')
|
||||
|
||||
@@ -13,6 +13,7 @@ orga_router = routers.DefaultRouter()
|
||||
orga_router.register(r'events', event.EventViewSet)
|
||||
|
||||
event_router = routers.DefaultRouter()
|
||||
event_router.register(r'subevents', event.SubEventViewSet)
|
||||
event_router.register(r'items', item.ItemViewSet)
|
||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||
event_router.register(r'questions', item.QuestionViewSet)
|
||||
@@ -21,6 +22,7 @@ event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
event_router.register(r'orders', order.OrderViewSet)
|
||||
event_router.register(r'orderpositions', order.OrderPositionViewSet)
|
||||
event_router.register(r'invoices', order.InvoiceViewSet)
|
||||
event_router.register(r'taxrules', event.TaxRuleViewSet)
|
||||
event_router.register(r'waitinglistentries', waitinglist.WaitingListViewSet)
|
||||
|
||||
# Force import of all plugins to give them a chance to register URLs with the router
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from rest_framework import viewsets
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import filters, viewsets
|
||||
|
||||
from pretix.api.serializers.event import EventSerializer
|
||||
from pretix.base.models import Event
|
||||
from pretix.api.serializers.event import (
|
||||
EventSerializer, SubEventSerializer, TaxRuleSerializer,
|
||||
)
|
||||
from pretix.base.models import Event, ItemCategory, TaxRule
|
||||
from pretix.base.models.event import SubEvent
|
||||
|
||||
|
||||
class EventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -11,4 +15,30 @@ class EventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
lookup_url_kwarg = 'event'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.events.all()
|
||||
return self.request.organizer.events.prefetch_related('meta_values', 'meta_values__property')
|
||||
|
||||
|
||||
class SubEventFilter(FilterSet):
|
||||
class Meta:
|
||||
model = SubEvent
|
||||
fields = ['active']
|
||||
|
||||
|
||||
class SubEventViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = SubEventSerializer
|
||||
queryset = ItemCategory.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, filters.OrderingFilter)
|
||||
filter_class = SubEventFilter
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.subevents.prefetch_related(
|
||||
'subeventitem_set', 'subeventitemvariation_set'
|
||||
)
|
||||
|
||||
|
||||
class TaxRuleViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = TaxRuleSerializer
|
||||
queryset = TaxRule.objects.none()
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.tax_rules.all()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import detail_route
|
||||
@@ -12,6 +14,14 @@ from pretix.base.models import Item, ItemCategory, Question, Quota
|
||||
|
||||
|
||||
class ItemFilter(FilterSet):
|
||||
tax_rate = django_filters.CharFilter(method='tax_rate_qs')
|
||||
|
||||
def tax_rate_qs(self, queryset, name, value):
|
||||
if value in ("0", "None", "0.00"):
|
||||
return queryset.filter(Q(tax_rule__isnull=True) | Q(tax_rule__rate=0))
|
||||
else:
|
||||
return queryset.filter(tax_rule__rate=value)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
fields = ['active', 'category', 'admission', 'tax_rate', 'free_price']
|
||||
@@ -26,7 +36,7 @@ class ItemViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_class = ItemFilter
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.items.prefetch_related('variations', 'addons').all()
|
||||
return self.request.event.items.select_related('tax_rule').prefetch_related('variations', 'addons').all()
|
||||
|
||||
|
||||
class ItemCategoryFilter(FilterSet):
|
||||
@@ -58,10 +68,17 @@ class QuestionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return self.request.event.questions.prefetch_related('options').all()
|
||||
|
||||
|
||||
class QuotaFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Quota
|
||||
fields = ['subevent']
|
||||
|
||||
|
||||
class QuotaViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = QuotaSerializer
|
||||
queryset = Quota.objects.none()
|
||||
filter_backends = (OrderingFilter,)
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
filter_class = QuotaFilter
|
||||
ordering_fields = ('id', 'size')
|
||||
ordering = ('id',)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.http import FileResponse
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from rest_framework import viewsets
|
||||
@@ -36,7 +37,8 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.orders.prefetch_related(
|
||||
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options'
|
||||
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
|
||||
'fees'
|
||||
).select_related(
|
||||
'invoice_address'
|
||||
)
|
||||
@@ -84,7 +86,7 @@ class OrderPositionFilter(FilterSet):
|
||||
class Meta:
|
||||
model = OrderPosition
|
||||
fields = ['item', 'variation', 'attendee_name', 'secret', 'order', 'order__status', 'has_checkin',
|
||||
'addon_to']
|
||||
'addon_to', 'subevent']
|
||||
|
||||
|
||||
class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@@ -137,12 +139,21 @@ class OrderPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
|
||||
class InvoiceFilter(FilterSet):
|
||||
refers = django_filters.CharFilter(name='refers', lookup_expr='invoice_no__iexact')
|
||||
refers = django_filters.CharFilter(method='refers_qs')
|
||||
number = django_filters.CharFilter(method='nr_qs')
|
||||
order = django_filters.CharFilter(name='order', lookup_expr='code__iexact')
|
||||
|
||||
def refers_qs(self, queryset, name, value):
|
||||
return queryset.annotate(
|
||||
refers_nr=Concat('refers__prefix', 'refers__invoice_no')
|
||||
).filter(refers_nr__iexact=value)
|
||||
|
||||
def nr_qs(self, queryset, name, value):
|
||||
return queryset.filter(nr__iexact=value)
|
||||
|
||||
class Meta:
|
||||
model = Invoice
|
||||
fields = ['order', 'invoice_no', 'is_cancellation', 'refers', 'locale']
|
||||
fields = ['order', 'number', 'is_cancellation', 'refers', 'locale']
|
||||
|
||||
|
||||
class RetryException(APIException):
|
||||
@@ -155,15 +166,17 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = InvoiceSerializer
|
||||
queryset = Invoice.objects.none()
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter)
|
||||
ordering = ('invoice_no',)
|
||||
ordering_fields = ('invoice_no', 'date')
|
||||
ordering = ('nr',)
|
||||
ordering_fields = ('nr', 'date')
|
||||
filter_class = InvoiceFilter
|
||||
lookup_field = 'invoice_no'
|
||||
lookup_url_kwarg = 'invoice_no'
|
||||
permission = 'can_view_orders'
|
||||
lookup_url_kwarg = 'number'
|
||||
lookup_field = 'nr'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.invoices.prefetch_related('lines').select_related('order')
|
||||
return self.request.event.invoices.prefetch_related('lines').select_related('order', 'refers').annotate(
|
||||
nr=Concat('prefix', 'invoice_no')
|
||||
)
|
||||
|
||||
@detail_route()
|
||||
def download(self, request, **kwargs):
|
||||
|
||||
@@ -16,7 +16,7 @@ class VoucherFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Voucher
|
||||
fields = ['code', 'max_usages', 'redeemed', 'block_quota', 'allow_ignore_quota',
|
||||
'price_mode', 'value', 'item', 'variation', 'quota', 'tag']
|
||||
'price_mode', 'value', 'item', 'variation', 'quota', 'tag', 'subevent']
|
||||
|
||||
def filter_active(self, queryset, name, value):
|
||||
if value:
|
||||
|
||||
@@ -15,7 +15,7 @@ class WaitingListFilter(FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher']
|
||||
fields = ['item', 'variation', 'email', 'locale', 'has_voucher', 'subevent']
|
||||
|
||||
|
||||
class WaitingListViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class PretixBaseConfig(AppConfig):
|
||||
@@ -9,13 +10,18 @@ class PretixBaseConfig(AppConfig):
|
||||
from . import exporter # NOQA
|
||||
from . import payment # NOQA
|
||||
from . import exporters # NOQA
|
||||
from .services import export, mail, tickets, cart, orders, cleanup, update_check # NOQA
|
||||
from . import invoice # NOQA
|
||||
from .services import export, mail, tickets, cart, orders, invoices, cleanup, update_check, quotas # NOQA
|
||||
|
||||
try:
|
||||
from .celery_app import app as celery_app # NOQA
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if hasattr(settings, 'RAVEN_CONFIG'):
|
||||
from ..sentry import initialize
|
||||
initialize()
|
||||
|
||||
|
||||
default_app_config = 'pretix.base.PretixBaseConfig'
|
||||
try:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from .answers import * # noqa
|
||||
from .invoices import * # noqa
|
||||
from .json import * # noqa
|
||||
from .mail import * # noqa
|
||||
|
||||
61
src/pretix/base/exporters/answers.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import os
|
||||
import tempfile
|
||||
from collections import OrderedDict
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django import forms
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import QuestionAnswer
|
||||
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
|
||||
|
||||
class AnswerFilesExporter(BaseExporter):
|
||||
identifier = 'answerfiles'
|
||||
verbose_name = _('Answers to file upload questions')
|
||||
|
||||
@property
|
||||
def export_form_fields(self):
|
||||
return OrderedDict(
|
||||
[
|
||||
('questions',
|
||||
forms.ModelMultipleChoiceField(
|
||||
queryset=self.event.questions.filter(type='F'),
|
||||
label=_('Questions'),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False
|
||||
)),
|
||||
]
|
||||
)
|
||||
|
||||
def render(self, form_data: dict):
|
||||
qs = QuestionAnswer.objects.filter(
|
||||
orderposition__order__event=self.event,
|
||||
).select_related('orderposition', 'orderposition__order', 'question')
|
||||
if form_data.get('questions'):
|
||||
qs = qs.filter(question__in=form_data['questions'])
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
with ZipFile(os.path.join(d, 'tmp.zip'), 'w') as zipf:
|
||||
for i in qs:
|
||||
if i.file:
|
||||
i.file.open('rb')
|
||||
fname = '{}-{}-{}-q{}-{}'.format(
|
||||
self.event.slug.upper(),
|
||||
i.orderposition.order.code,
|
||||
i.orderposition.positionid,
|
||||
i.question.pk,
|
||||
os.path.basename(i.file.name).split('.', 1)[1]
|
||||
)
|
||||
zipf.writestr(fname, i.file.read())
|
||||
i.file.close()
|
||||
|
||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||
return 'answers.zip', 'application/zip', zipf.read()
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_answers")
|
||||
def register_anwers_export(sender, **kwargs):
|
||||
return AnswerFilesExporter
|
||||
@@ -21,7 +21,7 @@ class InvoiceExporter(BaseExporter):
|
||||
if not i.file:
|
||||
invoice_pdf_task.apply(args=(i.pk,))
|
||||
i.refresh_from_db()
|
||||
i.file.open('r')
|
||||
i.file.open('rb')
|
||||
zipf.writestr('{}.pdf'.format(i.number), i.file.read())
|
||||
i.file.close()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.dispatch import receiver
|
||||
@@ -32,7 +33,8 @@ class JSONExporter(BaseExporter):
|
||||
'name': str(item.name),
|
||||
'category': item.category_id,
|
||||
'price': item.default_price,
|
||||
'tax_rate': item.tax_rate,
|
||||
'tax_rate': item.tax_rule.rate if item.tax_rule else Decimal('0.00'),
|
||||
'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
|
||||
'admission': item.admission,
|
||||
'active': item.active,
|
||||
'variations': [
|
||||
@@ -44,7 +46,7 @@ class JSONExporter(BaseExporter):
|
||||
'name': str(variation)
|
||||
} for variation in item.variations.all()
|
||||
]
|
||||
} for item in self.event.items.all().prefetch_related('variations')
|
||||
} for item in self.event.items.select_related('tax_rule').prefetch_related('variations')
|
||||
],
|
||||
'questions': [
|
||||
{
|
||||
@@ -59,7 +61,13 @@ class JSONExporter(BaseExporter):
|
||||
'status': order.status,
|
||||
'user': order.email,
|
||||
'datetime': order.datetime,
|
||||
'payment_fee': order.payment_fee,
|
||||
'fees': [
|
||||
{
|
||||
'type': fee.fee_type,
|
||||
'description': fee.description,
|
||||
'value': fee.value,
|
||||
} for fee in order.fees.all()
|
||||
],
|
||||
'total': order.total,
|
||||
'positions': [
|
||||
{
|
||||
@@ -80,7 +88,7 @@ class JSONExporter(BaseExporter):
|
||||
} for position in order.positions.all()
|
||||
]
|
||||
} for order in
|
||||
self.event.orders.all().prefetch_related('positions', 'positions__answers')
|
||||
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'fees')
|
||||
],
|
||||
'quotas': [
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import csv
|
||||
import io
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.db.models import Sum
|
||||
from django.dispatch import receiver
|
||||
@@ -11,6 +11,7 @@ from django.utils.formats import localize
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from pretix.base.models import InvoiceAddress, Order, OrderPosition
|
||||
from pretix.base.models.orders import OrderFee
|
||||
|
||||
from ..exporter import BaseExporter
|
||||
from ..signals import register_data_exporters
|
||||
@@ -35,7 +36,10 @@ class OrderListExporter(BaseExporter):
|
||||
|
||||
def _get_all_tax_rates(self, qs):
|
||||
tax_rates = set(
|
||||
qs.exclude(payment_fee=0).values_list('payment_fee_tax_rate', flat=True).distinct().order_by()
|
||||
a for a
|
||||
in OrderFee.objects.filter(
|
||||
order__event=self.event
|
||||
).values_list('tax_rate', flat=True).distinct().order_by()
|
||||
)
|
||||
tax_rates |= set(
|
||||
a for a
|
||||
@@ -59,7 +63,7 @@ class OrderListExporter(BaseExporter):
|
||||
headers = [
|
||||
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
|
||||
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
|
||||
_('Payment date'), _('Payment type'), _('Payment method fee'),
|
||||
_('Payment date'), _('Payment type'), _('Fees'),
|
||||
]
|
||||
|
||||
for tr in tax_rates:
|
||||
@@ -78,6 +82,16 @@ class OrderListExporter(BaseExporter):
|
||||
for k, v in self.event.get_payment_providers().items()
|
||||
}
|
||||
|
||||
full_fee_sum_cache = {
|
||||
o['order__id']: o['grosssum'] for o in
|
||||
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(grosssum=Sum('value'))
|
||||
}
|
||||
fee_sum_cache = {
|
||||
(o['order__id'], o['tax_rate']): o for o in
|
||||
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(
|
||||
taxsum=Sum('tax_value'), grosssum=Sum('value')
|
||||
)
|
||||
}
|
||||
sum_cache = {
|
||||
(o['order__id'], o['tax_rate']): o for o in
|
||||
OrderPosition.objects.values('tax_rate', 'order__id').order_by().annotate(
|
||||
@@ -100,7 +114,7 @@ class OrderListExporter(BaseExporter):
|
||||
order.invoice_address.street,
|
||||
order.invoice_address.zipcode,
|
||||
order.invoice_address.city,
|
||||
order.invoice_address.country,
|
||||
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country_old,
|
||||
order.invoice_address.vat_id,
|
||||
]
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
@@ -109,19 +123,18 @@ class OrderListExporter(BaseExporter):
|
||||
row += [
|
||||
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
|
||||
provider_names.get(order.payment_provider, order.payment_provider),
|
||||
localize(order.payment_fee)
|
||||
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00'))
|
||||
]
|
||||
|
||||
for tr in tax_rates:
|
||||
taxrate_values = sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
||||
if tr == order.payment_fee_tax_rate and order.payment_fee_tax_value:
|
||||
taxrate_values['grosssum'] += order.payment_fee
|
||||
taxrate_values['taxsum'] += order.payment_fee_tax_value
|
||||
fee_taxrate_values = fee_sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
|
||||
|
||||
row += [
|
||||
localize(taxrate_values['grosssum']),
|
||||
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']),
|
||||
localize(taxrate_values['taxsum']),
|
||||
localize(taxrate_values['grosssum'] + fee_taxrate_values['grosssum']),
|
||||
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']
|
||||
+ fee_taxrate_values['grosssum'] - fee_taxrate_values['taxsum']),
|
||||
localize(taxrate_values['taxsum'] + fee_taxrate_values['taxsum']),
|
||||
]
|
||||
|
||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.utils.crypto import get_random_string
|
||||
from hierarkey.forms import HierarkeyForm
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
|
||||
from .validators import PlaceholderValidator # NOQA
|
||||
|
||||
@@ -56,6 +57,9 @@ class SettingsForm(i18nfield.forms.I18nFormMixin, HierarkeyForm):
|
||||
kwargs['locales'] = self.locales
|
||||
kwargs['initial'] = self.obj.settings.freeze()
|
||||
super().__init__(*args, **kwargs)
|
||||
for f in self.fields.values():
|
||||
if isinstance(f, (RelativeDateTimeField, RelativeDateField)):
|
||||
f.set_event(self.obj)
|
||||
|
||||
def get_new_filename(self, name: str) -> str:
|
||||
nonce = get_random_string(length=8)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
@@ -15,6 +16,7 @@ class LoginForm(forms.Form):
|
||||
"""
|
||||
email = forms.EmailField(label=_("E-mail"), max_length=254)
|
||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("Please enter a correct email address and password."),
|
||||
@@ -29,6 +31,8 @@ class LoginForm(forms.Form):
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
del self.fields['keep_logged_in']
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
@@ -90,6 +94,12 @@ class RegistrationForm(forms.Form):
|
||||
}),
|
||||
required=True
|
||||
)
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
del self.fields['keep_logged_in']
|
||||
|
||||
def clean(self):
|
||||
password1 = self.cleaned_data.get('password', '')
|
||||
|
||||
478
src/pretix/base/invoice.py
Normal file
@@ -0,0 +1,478 @@
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
from typing import Tuple
|
||||
|
||||
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 pgettext
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.enums import TA_LEFT
|
||||
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.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import (
|
||||
BaseDocTemplate, Frame, KeepTogether, NextPageTemplate, PageTemplate,
|
||||
Paragraph, Spacer, Table, TableStyle,
|
||||
)
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Invoice
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
|
||||
|
||||
class BaseInvoiceRenderer:
|
||||
"""
|
||||
This is the base class for all invoice renderers.
|
||||
"""
|
||||
|
||||
def __init__(self, event: Event):
|
||||
self.event = event
|
||||
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
def generate(self, invoice: Invoice) -> Tuple[str, str, str]:
|
||||
"""
|
||||
This method should generate the invoice file and return a tuple consisting of a
|
||||
filename, a file type and file content. The extension will be taken from the filename
|
||||
which is otherwise ignored.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
"""
|
||||
A human-readable name for this renderer. This should be short but
|
||||
self-explanatory. Good examples include 'German DIN 5008' or 'Italian invoice'.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
"""
|
||||
A short and unique identifier for this renderer.
|
||||
This should only contain lowercase letters and in most
|
||||
cases will be the same as your package name.
|
||||
"""
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
|
||||
class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
"""
|
||||
This is a convenience class to avoid duplicate code when implementing invoice renderers
|
||||
that are based on reportlab.
|
||||
"""
|
||||
pagesize = pagesizes.A4
|
||||
left_margin = 25 * mm
|
||||
right_margin = 20 * mm
|
||||
top_margin = 20 * mm
|
||||
bottom_margin = 15 * mm
|
||||
doc_template_class = BaseDocTemplate
|
||||
|
||||
def _init(self):
|
||||
"""
|
||||
Initialize the renderer. By default, this registers fonts and sets ``self.stylesheet``.
|
||||
"""
|
||||
self.stylesheet = self._get_stylesheet()
|
||||
self._register_fonts()
|
||||
|
||||
def _get_stylesheet(self):
|
||||
"""
|
||||
Get a stylesheet. By default, this contains the "Normal" and "Heading1" styles.
|
||||
"""
|
||||
stylesheet = StyleSheet1()
|
||||
stylesheet.add(ParagraphStyle(name='Normal', fontName='OpenSans', fontSize=10, leading=12))
|
||||
stylesheet.add(ParagraphStyle(name='Heading1', fontName='OpenSansBd', fontSize=15, leading=15 * 1.2))
|
||||
stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName='OpenSansBd', fontSize=8, leading=12))
|
||||
stylesheet.add(ParagraphStyle(name='Fineprint', fontName='OpenSans', fontSize=8, leading=10))
|
||||
return stylesheet
|
||||
|
||||
def _register_fonts(self):
|
||||
"""
|
||||
Register fonts with reportlab. By default, this registers the OpenSans font family
|
||||
"""
|
||||
pdfmetrics.registerFont(TTFont('OpenSans', finders.find('fonts/OpenSans-Regular.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('OpenSansIt', finders.find('fonts/OpenSans-Italic.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('OpenSansBd', finders.find('fonts/OpenSans-Bold.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('OpenSansBI', finders.find('fonts/OpenSans-BoldItalic.ttf')))
|
||||
pdfmetrics.registerFontFamily('OpenSans', normal='OpenSans', bold='OpenSansBd',
|
||||
italic='OpenSansIt', boldItalic='OpenSansBI')
|
||||
|
||||
def _on_other_page(self, canvas: Canvas, doc):
|
||||
"""
|
||||
Called when a new page is rendered that is *not* the first page.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _on_first_page(self, canvas: Canvas, doc):
|
||||
"""
|
||||
Called when a new page is rendered that is the first page.
|
||||
"""
|
||||
pass
|
||||
|
||||
def _get_story(self, doc):
|
||||
"""
|
||||
Called to create the story to be inserted into the main frames.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_first_page_frames(self, doc):
|
||||
"""
|
||||
Called to create a list of frames for the first page.
|
||||
"""
|
||||
return [
|
||||
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height - 75 * mm,
|
||||
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
|
||||
id='normal')
|
||||
]
|
||||
|
||||
def _get_other_page_frames(self, doc):
|
||||
"""
|
||||
Called to create a list of frames for the other pages.
|
||||
"""
|
||||
return [
|
||||
Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height,
|
||||
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=0,
|
||||
id='normal')
|
||||
]
|
||||
|
||||
def _build_doc(self, fhandle):
|
||||
"""
|
||||
Build a PDF document in a given file handle
|
||||
"""
|
||||
self._init()
|
||||
doc = self.doc_template_class(fhandle, pagesize=self.pagesize,
|
||||
leftMargin=self.left_margin, rightMargin=self.right_margin,
|
||||
topMargin=self.top_margin, bottomMargin=self.bottom_margin)
|
||||
|
||||
doc.addPageTemplates([
|
||||
PageTemplate(
|
||||
id='FirstPage',
|
||||
frames=self._get_first_page_frames(doc),
|
||||
onPage=self._on_first_page,
|
||||
pagesize=self.pagesize
|
||||
),
|
||||
PageTemplate(
|
||||
id='OtherPages',
|
||||
frames=self._get_other_page_frames(doc),
|
||||
onPage=self._on_other_page,
|
||||
pagesize=self.pagesize
|
||||
)
|
||||
])
|
||||
story = self._get_story(doc)
|
||||
doc.build(story)
|
||||
return doc
|
||||
|
||||
def generate(self, invoice: Invoice):
|
||||
self.invoice = invoice
|
||||
buffer = BytesIO()
|
||||
self._build_doc(buffer)
|
||||
buffer.seek(0)
|
||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||
|
||||
|
||||
class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
identifier = 'classic'
|
||||
verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)')
|
||||
|
||||
def _on_other_page(self, canvas: Canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.setFont('OpenSans', 8)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
|
||||
|
||||
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())
|
||||
|
||||
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()
|
||||
canvas.setFont('OpenSans', 8)
|
||||
canvas.drawRightString(self.pagesize[0] - 20 * mm, 10 * mm, pgettext("invoice", "Page %d") % (doc.page,))
|
||||
|
||||
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())
|
||||
|
||||
textobject = canvas.beginText(25 * mm, (297 - 15) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice from').upper())
|
||||
canvas.drawText(textobject)
|
||||
|
||||
p = Paragraph(self.invoice.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])
|
||||
|
||||
textobject = canvas.beginText(25 * mm, (297 - 50) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice to').upper())
|
||||
canvas.drawText(textobject)
|
||||
|
||||
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])
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 38) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Order code').upper())
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSans', 10)
|
||||
textobject.textLine(self.invoice.order.full_code)
|
||||
canvas.drawText(textobject)
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 50) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.textLine(pgettext('invoice', 'Cancellation number').upper())
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSans', 10)
|
||||
textobject.textLine(self.invoice.number)
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Original invoice').upper())
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSans', 10)
|
||||
textobject.textLine(self.invoice.refers.number)
|
||||
else:
|
||||
textobject.textLine(pgettext('invoice', 'Invoice number').upper())
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSans', 10)
|
||||
textobject.textLine(self.invoice.number)
|
||||
textobject.moveCursor(0, 5)
|
||||
|
||||
if self.invoice.is_cancellation:
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Cancellation date').upper())
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSans', 10)
|
||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Original invoice date').upper())
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSans', 10)
|
||||
textobject.textLine(date_format(self.invoice.refers.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
else:
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Invoice date').upper())
|
||||
textobject.moveCursor(0, 5)
|
||||
textobject.setFont('OpenSans', 10)
|
||||
textobject.textLine(date_format(self.invoice.date, "DATE_FORMAT"))
|
||||
textobject.moveCursor(0, 5)
|
||||
|
||||
canvas.drawText(textobject)
|
||||
|
||||
if self.invoice.event.settings.invoice_logo_image:
|
||||
logo_file = self.invoice.event.settings.get('invoice_logo_image', binary_file=True)
|
||||
canvas.drawImage(ImageReader(logo_file),
|
||||
95 * mm, (297 - 38) * mm,
|
||||
width=25 * mm, height=25 * mm,
|
||||
preserveAspectRatio=True, anchor='n',
|
||||
mask='auto')
|
||||
|
||||
if self.invoice.event.settings.show_date_to:
|
||||
p_str = (
|
||||
str(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())
|
||||
)
|
||||
else:
|
||||
p_str = (
|
||||
str(self.invoice.event.name) + '\n' + self.invoice.event.get_date_from_display()
|
||||
)
|
||||
|
||||
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])
|
||||
|
||||
textobject = canvas.beginText(125 * mm, (297 - 15) * mm)
|
||||
textobject.setFont('OpenSansBd', 8)
|
||||
textobject.textLine(pgettext('invoice', 'Event').upper())
|
||||
canvas.drawText(textobject)
|
||||
|
||||
canvas.restoreState()
|
||||
|
||||
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 - 75 * mm,
|
||||
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
|
||||
id='normal')
|
||||
]
|
||||
|
||||
def _get_other_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,
|
||||
leftPadding=0, rightPadding=0, topPadding=0, bottomPadding=footer_length,
|
||||
id='normal')
|
||||
]
|
||||
|
||||
def _get_story(self, doc):
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
Paragraph(pgettext('invoice', 'Invoice')
|
||||
if not self.invoice.is_cancellation
|
||||
else pgettext('invoice', 'Cancellation'),
|
||||
self.stylesheet['Heading1']),
|
||||
Spacer(1, 5 * mm),
|
||||
NextPageTemplate('OtherPages'),
|
||||
]
|
||||
|
||||
if self.invoice.introductory_text:
|
||||
story.append(Paragraph(self.invoice.introductory_text, self.stylesheet['Normal']))
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
taxvalue_map = defaultdict(Decimal)
|
||||
grossvalue_map = defaultdict(Decimal)
|
||||
|
||||
tstyledata = [
|
||||
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
||||
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'OpenSansBd'),
|
||||
('FONTNAME', (0, -1), (-1, -1), 'OpenSansBd'),
|
||||
('LEFTPADDING', (0, 0), (0, -1), 0),
|
||||
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
|
||||
]
|
||||
tdata = [(
|
||||
pgettext('invoice', 'Description'),
|
||||
pgettext('invoice', 'Tax rate'),
|
||||
pgettext('invoice', 'Net'),
|
||||
pgettext('invoice', 'Gross'),
|
||||
)]
|
||||
total = Decimal('0.00')
|
||||
for line in self.invoice.lines.all():
|
||||
tdata.append((
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
localize(line.tax_rate) + " %",
|
||||
localize(line.net_value) + " " + self.invoice.event.currency,
|
||||
localize(line.gross_value) + " " + self.invoice.event.currency,
|
||||
))
|
||||
taxvalue_map[line.tax_rate, line.tax_name] += line.tax_value
|
||||
grossvalue_map[line.tax_rate, line.tax_name] += line.gross_value
|
||||
total += line.gross_value
|
||||
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', '', localize(total) + " " + self.invoice.event.currency
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.55, .15, .15, .15)]
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=1)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
story.append(table)
|
||||
|
||||
story.append(Spacer(1, 15 * mm))
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(Paragraph(self.invoice.payment_provider_text, self.stylesheet['Normal']))
|
||||
|
||||
if self.invoice.additional_text:
|
||||
story.append(Paragraph(self.invoice.additional_text, self.stylesheet['Normal']))
|
||||
story.append(Spacer(1, 15 * mm))
|
||||
|
||||
tstyledata = [
|
||||
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
||||
('LEFTPADDING', (0, 0), (0, -1), 0),
|
||||
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 8),
|
||||
('FONTNAME', (0, 0), (-1, -1), 'OpenSans'),
|
||||
]
|
||||
thead = [
|
||||
pgettext('invoice', 'Tax rate'),
|
||||
pgettext('invoice', 'Net value'),
|
||||
pgettext('invoice', 'Gross value'),
|
||||
pgettext('invoice', 'Tax'),
|
||||
''
|
||||
]
|
||||
tdata = [thead]
|
||||
|
||||
for idx, gross in grossvalue_map.items():
|
||||
rate, name = idx
|
||||
if rate == 0:
|
||||
continue
|
||||
tax = taxvalue_map[idx]
|
||||
tdata.append([
|
||||
localize(rate) + " % " + name,
|
||||
localize(gross - tax) + " " + self.invoice.event.currency,
|
||||
localize(gross) + " " + self.invoice.event.currency,
|
||||
localize(tax) + " " + self.invoice.event.currency,
|
||||
''
|
||||
])
|
||||
|
||||
def fmt(val):
|
||||
try:
|
||||
return vat_moss.exchange_rates.format(val, self.invoice.foreign_currency_display)
|
||||
except ValueError:
|
||||
return localize(val) + ' ' + self.invoice.foreign_currency_display
|
||||
|
||||
if len(tdata) > 1:
|
||||
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(KeepTogether([
|
||||
Paragraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
||||
table
|
||||
]))
|
||||
|
||||
if self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||
tdata = [thead]
|
||||
|
||||
for idx, gross in grossvalue_map.items():
|
||||
rate, name = idx
|
||||
if rate == 0:
|
||||
continue
|
||||
tax = taxvalue_map[idx]
|
||||
gross = round_decimal(gross * self.invoice.foreign_currency_rate)
|
||||
tax = round_decimal(tax * self.invoice.foreign_currency_rate)
|
||||
net = gross - tax
|
||||
|
||||
tdata.append([
|
||||
localize(rate) + " % " + name,
|
||||
fmt(net), fmt(gross), fmt(tax), ''
|
||||
])
|
||||
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=2, hAlign=TA_LEFT)
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
|
||||
story.append(KeepTogether([
|
||||
Spacer(1, height=2 * mm),
|
||||
Paragraph(
|
||||
pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the European Central Bank on '
|
||||
'{date}, this corresponds to:'
|
||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
|
||||
self.stylesheet['Fineprint']
|
||||
),
|
||||
Spacer(1, height=3 * mm),
|
||||
table
|
||||
]))
|
||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||
story.append(Spacer(1, 5 * mm))
|
||||
story.append(Paragraph(
|
||||
pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the European Central Bank on '
|
||||
'{date}, the invoice total corresponds to {total}.'
|
||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
||||
total=fmt(total)),
|
||||
self.stylesheet['Fineprint']
|
||||
))
|
||||
|
||||
return story
|
||||
|
||||
|
||||
@receiver(register_invoice_renderers, dispatch_uid="invoice_renderer_classic")
|
||||
def recv_classic(sender, **kwargs):
|
||||
return ClassicInvoiceRenderer
|
||||
@@ -7,6 +7,7 @@ from django.core.urlresolvers import get_script_prefix
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||
from django.utils.translation.trans_real import (
|
||||
@@ -132,8 +133,8 @@ def get_language_from_request(request: HttpRequest) -> str:
|
||||
return (
|
||||
get_language_from_user_settings(request)
|
||||
or get_language_from_session_or_cookie(request)
|
||||
or get_language_from_event(request)
|
||||
or get_language_from_browser(request)
|
||||
or get_language_from_event(request)
|
||||
or get_default_language()
|
||||
)
|
||||
|
||||
@@ -165,6 +166,9 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'/api/v1/docs/',
|
||||
)
|
||||
|
||||
def process_request(self, request):
|
||||
request.csp_nonce = get_random_string(length=32)
|
||||
|
||||
def process_response(self, request, resp):
|
||||
if settings.DEBUG and resp.status_code >= 400:
|
||||
# Don't use CSP on debug error page as it breaks of Django's fancy error
|
||||
@@ -179,20 +183,25 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# frame-src is deprecated but kept for compatibility with CSP 1.0 browsers, e.g. Safari 9
|
||||
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'style-src': ["{static}"],
|
||||
'connect-src': ["{dynamic}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "data:", "https://*.stripe.com"],
|
||||
'style-src': ["{static}", "{media}", "'nonce-{nonce}'"],
|
||||
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
|
||||
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
|
||||
'font-src': ["{static}"],
|
||||
# form-action is not only used to match on form actions, but also on URLs
|
||||
# form-actions redirect to. In the context of e.g. payment providers or
|
||||
# single-sign-on this can be nearly anything so we cannot really restrict
|
||||
# this. However, we'll restrict it to HTTPS.
|
||||
'form-action': ["{dynamic}", "https:"],
|
||||
'report-uri': ["/csp_report/"],
|
||||
}
|
||||
if 'Content-Security-Policy' in resp:
|
||||
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
|
||||
|
||||
staticdomain = "'self'"
|
||||
dynamicdomain = "'self'"
|
||||
mediadomain = "'self'"
|
||||
if settings.MEDIA_URL.startswith('http'):
|
||||
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
|
||||
if settings.STATIC_URL.startswith('http'):
|
||||
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
||||
if settings.SITE_URL.startswith('http'):
|
||||
@@ -211,6 +220,14 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||
dynamicdomain += " " + domain
|
||||
|
||||
if request.path not in self.CSP_EXEMPT:
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain)
|
||||
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
|
||||
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
|
||||
media=mediadomain, nonce=request.csp_nonce)
|
||||
for k, v in h.items():
|
||||
h[k] = ' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain,
|
||||
nonce=request.csp_nonce).split(' ')
|
||||
resp['Content-Security-Policy'] = _render_csp(h)
|
||||
elif 'Content-Security-Policy' in resp:
|
||||
del resp['Content-Security-Policy']
|
||||
|
||||
return resp
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-30 17:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
import django.db.migrations.operations.special
|
||||
import django.db.models.deletion
|
||||
import django_countries.fields
|
||||
import i18nfield.fields
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.event
|
||||
import pretix.base.models.invoices
|
||||
import pretix.base.models.orders
|
||||
import pretix.base.models.organizer
|
||||
import pretix.base.validators
|
||||
|
||||
|
||||
def create_teams(apps, schema_editor):
|
||||
Organizer = apps.get_model('pretixbase', 'Organizer')
|
||||
Team = apps.get_model('pretixbase', 'Team')
|
||||
|
||||
for o in Organizer.objects.prefetch_related('events'):
|
||||
for e in o.events.all():
|
||||
teams = {}
|
||||
|
||||
for p in e.user_perms.all():
|
||||
pkey = (p.can_change_settings, p.can_change_items, p.can_view_orders,
|
||||
p.can_change_permissions, p.can_change_orders, p.can_view_vouchers,
|
||||
p.can_change_vouchers)
|
||||
if pkey not in teams:
|
||||
team = Team()
|
||||
team.can_change_event_settings = p.can_change_settings
|
||||
team.can_change_items = p.can_change_items
|
||||
team.can_view_orders = p.can_view_orders
|
||||
team.can_change_orders = p.can_change_orders
|
||||
team.can_view_vouchers = p.can_view_vouchers
|
||||
team.can_change_vouchers = p.can_change_vouchers
|
||||
team.organizer = o
|
||||
team.name = '{} Team {}'.format(
|
||||
str(e.name), len(teams) + 1
|
||||
)
|
||||
team.save()
|
||||
team.limit_events.add(e)
|
||||
|
||||
teams[pkey] = team
|
||||
|
||||
if p.user:
|
||||
teams[pkey].members.add(p.user)
|
||||
else:
|
||||
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
|
||||
|
||||
teams = {}
|
||||
for p in o.user_perms.all():
|
||||
pkey = (p.can_create_events, p.can_change_permissions)
|
||||
if pkey not in teams:
|
||||
team = Team()
|
||||
team.can_change_organizer_settings = True
|
||||
team.can_create_events = p.can_create_events
|
||||
team.can_change_teams = p.can_change_permissions
|
||||
team.organizer = o
|
||||
team.name = '{} Team {}'.format(
|
||||
str(o.name), len(teams) + 1
|
||||
)
|
||||
team.save()
|
||||
teams[pkey] = team
|
||||
|
||||
if p.user:
|
||||
teams[pkey].members.add(p.user)
|
||||
else:
|
||||
teams[pkey].invites.create(email=p.invite_email, token=p.invite_token)
|
||||
|
||||
|
||||
def rename_placeholder(app, schema_editor):
|
||||
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
|
||||
|
||||
for setting in EventSettingsStore.objects.all():
|
||||
if setting.key == 'mail_text_order_placed':
|
||||
new_value = setting.value.replace('{paymentinfo}', '{payment_info}')
|
||||
setting.value = new_value
|
||||
cache.delete('hierarkey_{}_{}'.format('event', setting.object_id))
|
||||
setting.save()
|
||||
|
||||
|
||||
def fwd69(app, schema_editor):
|
||||
Event = app.get_model('pretixbase', 'Event')
|
||||
for e in Event.objects.select_related('organizer').all():
|
||||
e.invoices.all().update(prefix=e.slug.upper() + '-', organizer=e.organizer)
|
||||
|
||||
|
||||
def fwd70(app, schema_editor):
|
||||
InvoiceAddress = app.get_model('pretixbase', 'InvoiceAddress')
|
||||
for ia in InvoiceAddress.objects.all():
|
||||
if ia.company or ia.vat_id:
|
||||
ia.is_business = True
|
||||
ia.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
replaces = [('pretixbase', '0052_team_teaminvite'), ('pretixbase', '0058_auto_20170429_1020'),
|
||||
('pretixbase', '0059_checkin_nonce'), ('pretixbase', '0060_auto_20170510_1027'),
|
||||
('pretixbase', '0061_auto_20170521_0942'), ('pretixbase', '0062_auto_20170602_0948'),
|
||||
('pretixbase', '0063_auto_20170702_1711'), ('pretixbase', '0064_auto_20170703_0912'),
|
||||
('pretixbase', '0065_auto_20170707_0920'), ('pretixbase', '0066_auto_20170708_2102'),
|
||||
('pretixbase', '0067_auto_20170712_1610'), ('pretixbase', '0068_subevent_frontpage_text'),
|
||||
('pretixbase', '0069_invoice_prefix'), ('pretixbase', '0070_auto_20170719_0910')]
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0051_auto_20170206_2027_squashed_0057_auto_20170501_2116'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=190, verbose_name='Team name')),
|
||||
('all_events',
|
||||
models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
|
||||
('can_create_events', models.BooleanField(default=False, verbose_name='Can create events')),
|
||||
('can_change_teams', models.BooleanField(default=False, verbose_name='Can change permissions')),
|
||||
('can_change_organizer_settings',
|
||||
models.BooleanField(default=False, verbose_name='Can change organizer settings')),
|
||||
('can_change_event_settings',
|
||||
models.BooleanField(default=False, verbose_name='Can change event settings')),
|
||||
('can_change_items', models.BooleanField(default=False, verbose_name='Can change product settings')),
|
||||
('can_view_orders', models.BooleanField(default=False, verbose_name='Can view orders')),
|
||||
('can_change_orders', models.BooleanField(default=False, verbose_name='Can change orders')),
|
||||
('can_view_vouchers', models.BooleanField(default=False, verbose_name='Can view vouchers')),
|
||||
('can_change_vouchers', models.BooleanField(default=False, verbose_name='Can change vouchers')),
|
||||
('limit_events', models.ManyToManyField(to='pretixbase.Event', verbose_name='Limit to events')),
|
||||
('members', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL,
|
||||
verbose_name='Team members')),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='teams',
|
||||
to='pretixbase.Organizer')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Teams',
|
||||
'verbose_name': 'Team',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamInvite',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True)),
|
||||
('token',
|
||||
models.CharField(blank=True, default=pretix.base.models.organizer.generate_invite_token, max_length=64,
|
||||
null=True)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites',
|
||||
to='pretixbase.Team')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=create_teams,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventpermission',
|
||||
name='event',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventpermission',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizerpermission',
|
||||
name='organizer',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizerpermission',
|
||||
name='user',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='permitted',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organizer',
|
||||
name='permitted',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='can_change_teams',
|
||||
field=models.BooleanField(default=False, verbose_name='Can change teams and permissions'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='limit_events',
|
||||
field=models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events'),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='EventPermission',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='OrganizerPermission',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='checkin',
|
||||
name='nonce',
|
||||
field=models.CharField(blank=True, max_length=190, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='date_admission',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Admission time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='location',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=rename_placeholder,
|
||||
reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamAPIToken',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=190)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('token', models.CharField(default=pretix.base.models.organizer.generate_api_token, max_length=64)),
|
||||
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens',
|
||||
to='pretixbase.Team')),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='voucher',
|
||||
options={'ordering': ('code',), 'verbose_name': 'Voucher', 'verbose_name_plural': 'Vouchers'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='meta_info',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='meta_info',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Meta information'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='currency',
|
||||
field=models.CharField(choices=[('AED', 'AED - UAE Dirham'), ('AFN', 'AFN - Afghani'), ('ALL', 'ALL - Lek'),
|
||||
('AMD', 'AMD - Armenian Dram'),
|
||||
('ANG', 'ANG - Netherlands Antillean Guilder'),
|
||||
('AOA', 'AOA - Kwanza'), ('ARS', 'ARS - Argentine Peso'),
|
||||
('AUD', 'AUD - Australian Dollar'), ('AWG', 'AWG - Aruban Florin'),
|
||||
('AZN', 'AZN - Azerbaijanian Manat'), ('BAM', 'BAM - Convertible Mark'),
|
||||
('BBD', 'BBD - Barbados Dollar'), ('BDT', 'BDT - Taka'),
|
||||
('BGN', 'BGN - Bulgarian Lev'), ('BHD', 'BHD - Bahraini Dinar'),
|
||||
('BIF', 'BIF - Burundi Franc'), ('BMD', 'BMD - Bermudian Dollar'),
|
||||
('BND', 'BND - Brunei Dollar'), ('BOB', 'BOB - Boliviano'),
|
||||
('BRL', 'BRL - Brazilian Real'), ('BSD', 'BSD - Bahamian Dollar'),
|
||||
('BTN', 'BTN - Ngultrum'), ('BWP', 'BWP - Pula'),
|
||||
('BYN', 'BYN - Belarusian Ruble'), ('BZD', 'BZD - Belize Dollar'),
|
||||
('CAD', 'CAD - Canadian Dollar'), ('CDF', 'CDF - Congolese Franc'),
|
||||
('CHF', 'CHF - Swiss Franc'), ('CLP', 'CLP - Chilean Peso'),
|
||||
('CNY', 'CNY - Yuan Renminbi'), ('COP', 'COP - Colombian Peso'),
|
||||
('CRC', 'CRC - Costa Rican Colon'), ('CUC', 'CUC - Peso Convertible'),
|
||||
('CUP', 'CUP - Cuban Peso'), ('CVE', 'CVE - Cabo Verde Escudo'),
|
||||
('CZK', 'CZK - Czech Koruna'), ('DJF', 'DJF - Djibouti Franc'),
|
||||
('DKK', 'DKK - Danish Krone'), ('DOP', 'DOP - Dominican Peso'),
|
||||
('DZD', 'DZD - Algerian Dinar'), ('EGP', 'EGP - Egyptian Pound'),
|
||||
('ERN', 'ERN - Nakfa'), ('ETB', 'ETB - Ethiopian Birr'),
|
||||
('EUR', 'EUR - Euro'),
|
||||
('FJD', 'FJD - Fiji Dollar'), ('FKP', 'FKP - Falkland Islands Pound'),
|
||||
('GBP', 'GBP - Pound Sterling'), ('GEL', 'GEL - Lari'),
|
||||
('GHS', 'GHS - Ghana Cedi'), ('GIP', 'GIP - Gibraltar Pound'),
|
||||
('GMD', 'GMD - Dalasi'), ('GNF', 'GNF - Guinea Franc'),
|
||||
('GTQ', 'GTQ - Quetzal'), ('GYD', 'GYD - Guyana Dollar'),
|
||||
('HKD', 'HKD - Hong Kong Dollar'), ('HNL', 'HNL - Lempira'),
|
||||
('HRK', 'HRK - Kuna'), ('HTG', 'HTG - Gourde'), ('HUF', 'HUF - Forint'),
|
||||
('IDR', 'IDR - Rupiah'), ('ILS', 'ILS - New Israeli Sheqel'),
|
||||
('INR', 'INR - Indian Rupee'), ('IQD', 'IQD - Iraqi Dinar'),
|
||||
('IRR', 'IRR - Iranian Rial'), ('ISK', 'ISK - Iceland Krona'),
|
||||
('JMD', 'JMD - Jamaican Dollar'), ('JOD', 'JOD - Jordanian Dinar'),
|
||||
('JPY', 'JPY - Yen'), ('KES', 'KES - Kenyan Shilling'),
|
||||
('KGS', 'KGS - Som'),
|
||||
('KHR', 'KHR - Riel'), ('KMF', 'KMF - Comoro Franc'),
|
||||
('KPW', 'KPW - North Korean Won'), ('KRW', 'KRW - Won'),
|
||||
('KWD', 'KWD - Kuwaiti Dinar'), ('KYD', 'KYD - Cayman Islands Dollar'),
|
||||
('KZT', 'KZT - Tenge'), ('LAK', 'LAK - Kip'),
|
||||
('LBP', 'LBP - Lebanese Pound'),
|
||||
('LKR', 'LKR - Sri Lanka Rupee'), ('LRD', 'LRD - Liberian Dollar'),
|
||||
('LSL', 'LSL - Loti'), ('LYD', 'LYD - Libyan Dinar'),
|
||||
('MAD', 'MAD - Moroccan Dirham'), ('MDL', 'MDL - Moldovan Leu'),
|
||||
('MGA', 'MGA - Malagasy Ariary'), ('MKD', 'MKD - Denar'),
|
||||
('MMK', 'MMK - Kyat'),
|
||||
('MNT', 'MNT - Tugrik'), ('MOP', 'MOP - Pataca'), ('MRO', 'MRO - Ouguiya'),
|
||||
('MUR', 'MUR - Mauritius Rupee'), ('MVR', 'MVR - Rufiyaa'),
|
||||
('MWK', 'MWK - Malawi Kwacha'), ('MXN', 'MXN - Mexican Peso'),
|
||||
('MYR', 'MYR - Malaysian Ringgit'), ('MZN', 'MZN - Mozambique Metical'),
|
||||
('NAD', 'NAD - Namibia Dollar'), ('NGN', 'NGN - Naira'),
|
||||
('NIO', 'NIO - Cordoba Oro'), ('NOK', 'NOK - Norwegian Krone'),
|
||||
('NPR', 'NPR - Nepalese Rupee'), ('NZD', 'NZD - New Zealand Dollar'),
|
||||
('OMR', 'OMR - Rial Omani'), ('PAB', 'PAB - Balboa'), ('PEN', 'PEN - Sol'),
|
||||
('PGK', 'PGK - Kina'), ('PHP', 'PHP - Philippine Peso'),
|
||||
('PKR', 'PKR - Pakistan Rupee'), ('PLN', 'PLN - Zloty'),
|
||||
('PYG', 'PYG - Guarani'), ('QAR', 'QAR - Qatari Rial'),
|
||||
('RON', 'RON - Romanian Leu'), ('RSD', 'RSD - Serbian Dinar'),
|
||||
('RUB', 'RUB - Russian Ruble'), ('RWF', 'RWF - Rwanda Franc'),
|
||||
('SAR', 'SAR - Saudi Riyal'), ('SBD', 'SBD - Solomon Islands Dollar'),
|
||||
('SCR', 'SCR - Seychelles Rupee'), ('SDG', 'SDG - Sudanese Pound'),
|
||||
('SEK', 'SEK - Swedish Krona'), ('SGD', 'SGD - Singapore Dollar'),
|
||||
('SHP', 'SHP - Saint Helena Pound'), ('SLL', 'SLL - Leone'),
|
||||
('SOS', 'SOS - Somali Shilling'), ('SRD', 'SRD - Surinam Dollar'),
|
||||
('SSP', 'SSP - South Sudanese Pound'), ('STD', 'STD - Dobra'),
|
||||
('SVC', 'SVC - El Salvador Colon'), ('SYP', 'SYP - Syrian Pound'),
|
||||
('SZL', 'SZL - Lilangeni'), ('THB', 'THB - Baht'), ('TJS', 'TJS - Somoni'),
|
||||
('TMT', 'TMT - Turkmenistan New Manat'), ('TND', 'TND - Tunisian Dinar'),
|
||||
('TOP', 'TOP - Pa’anga'), ('TRY', 'TRY - Turkish Lira'),
|
||||
('TTD', 'TTD - Trinidad and Tobago Dollar'),
|
||||
('TWD', 'TWD - New Taiwan Dollar'),
|
||||
('TZS', 'TZS - Tanzanian Shilling'), ('UAH', 'UAH - Hryvnia'),
|
||||
('UGX', 'UGX - Uganda Shilling'), ('USD', 'USD - US Dollar'),
|
||||
('UYU', 'UYU - Peso Uruguayo'), ('UZS', 'UZS - Uzbekistan Sum'),
|
||||
('VEF', 'VEF - Bolívar'), ('VND', 'VND - Dong'), ('VUV', 'VUV - Vatu'),
|
||||
('WST', 'WST - Tala'), ('XAF', 'XAF - CFA Franc BEAC'),
|
||||
('XAG', 'XAG - Silver'),
|
||||
('XAU', 'XAU - Gold'),
|
||||
('XBA', 'XBA - Bond Markets Unit European Composite Unit (EURCO)'),
|
||||
('XBB', 'XBB - Bond Markets Unit European Monetary Unit (E.M.U.-6)'),
|
||||
('XBC', 'XBC - Bond Markets Unit European Unit of Account 9 (E.U.A.-9)'),
|
||||
('XBD', 'XBD - Bond Markets Unit European Unit of Account 17 (E.U.A.-17)'),
|
||||
('XCD', 'XCD - East Caribbean Dollar'),
|
||||
('XDR', 'XDR - SDR (Special Drawing Right)'),
|
||||
('XOF', 'XOF - CFA Franc BCEAO'),
|
||||
('XPD', 'XPD - Palladium'), ('XPF', 'XPF - CFP Franc'),
|
||||
('XPT', 'XPT - Platinum'), ('XSU', 'XSU - Sucre'),
|
||||
('XTS', 'XTS - Codes specifically reserved for testing purposes'),
|
||||
('XUA', 'XUA - ADB Unit of Account'), ('XXX',
|
||||
'XXX - The codes assigned for transactions where no currency is involved'),
|
||||
('YER', 'YER - Yemeni Rial'), ('ZAR', 'ZAR - Rand'),
|
||||
('ZMW', 'ZMW - Zambian Kwacha'), ('ZWL', 'ZWL - Zimbabwe Dollar')],
|
||||
default='EUR', max_length=10, verbose_name='Default currency'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='slug',
|
||||
field=models.SlugField(
|
||||
help_text='Should be short, only contain lowercase letters and numbers, and must be unique among your events. We recommend some kind of abbreviation or a date with less than 10 characters that can be easily remembered, but you can also choose to use a random value. This will be used in URLs, order codes, invoice numbers, and bank transfer references.',
|
||||
validators=[django.core.validators.RegexValidator(
|
||||
message='The slug may only contain letters, numbers, dots and dashes.', regex='^[a-zA-Z0-9.-]+$'),
|
||||
pretix.base.validators.EventSlugBlacklistValidator()], verbose_name='Short form'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='date',
|
||||
field=models.DateField(default=pretix.base.models.invoices.today),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='item',
|
||||
name='allow_cancel',
|
||||
field=models.BooleanField(default=True,
|
||||
help_text='If this is active and the general event settings allow it, orders containing this product can be canceled by the user until the order is paid for. Users cannot cancel paid orders on their own and you can cancel orders at all times, regardless of this setting',
|
||||
verbose_name='Allow product to be canceled'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionanswer',
|
||||
name='file',
|
||||
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.answerfile_name),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='type',
|
||||
field=models.CharField(
|
||||
choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'),
|
||||
('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload')],
|
||||
max_length=5, verbose_name='Question type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Internal comment'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.',
|
||||
null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='presale_start',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.',
|
||||
null=True, verbose_name='Start of presale'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SubEvent',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('active', models.BooleanField(default=False,
|
||||
help_text='Only with this checkbox enabled, this sub-event is visible in the frontend to users.',
|
||||
verbose_name='Active')),
|
||||
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
|
||||
('date_from', models.DateTimeField(verbose_name='Event start time')),
|
||||
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
|
||||
('date_admission', models.DateTimeField(blank=True, null=True, verbose_name='Admission time')),
|
||||
('presale_end',
|
||||
models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True,
|
||||
verbose_name='End of presale')),
|
||||
('presale_start',
|
||||
models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True,
|
||||
verbose_name='Start of presale')),
|
||||
(
|
||||
'location',
|
||||
i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Sub-Event',
|
||||
'verbose_name_plural': 'Sub-Events',
|
||||
'ordering': ('date_from', 'name'),
|
||||
},
|
||||
bases=(pretix.base.models.event.EventMixin, models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SubEventItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item')),
|
||||
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SubEventItemVariation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
|
||||
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
|
||||
(
|
||||
'variation',
|
||||
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='has_subevents',
|
||||
field=models.BooleanField(default=False, verbose_name='Event series'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subevents',
|
||||
to='pretixbase.Event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='items',
|
||||
field=models.ManyToManyField(through='pretixbase.SubEventItem', to='pretixbase.Item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='variations',
|
||||
field=models.ManyToManyField(through='pretixbase.SubEventItemVariation', to='pretixbase.ItemVariation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='quotas', to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
|
||||
to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='subevent',
|
||||
options={'ordering': ('date_from', 'name'), 'verbose_name': 'Date in event series',
|
||||
'verbose_name_plural': 'Dates in event series'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemaddon',
|
||||
name='price_included',
|
||||
field=models.BooleanField(default=False,
|
||||
help_text='If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost money individually.',
|
||||
verbose_name='Add-Ons are included in the price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='active',
|
||||
field=models.BooleanField(default=False,
|
||||
help_text='Only with this checkbox enabled, this date is visible in the frontend to users.',
|
||||
verbose_name='Active'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.',
|
||||
null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='presale_start',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.',
|
||||
null=True, verbose_name='Start of presale'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='frontpage_text',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, null=True, verbose_name='Frontpage text'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='prefix',
|
||||
field=models.CharField(db_index=True, default='', max_length=160),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='organizer',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='invoices',
|
||||
to='pretixbase.Organizer'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=fwd69, reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='invoice',
|
||||
unique_together=set([('organizer', 'prefix', 'invoice_no')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='organizer',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='invoices',
|
||||
to='pretixbase.Organizer'),
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='invoiceaddress',
|
||||
old_name='country',
|
||||
new_name='country_old',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(default='', max_length=2, verbose_name='Country'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='is_business',
|
||||
field=models.BooleanField(default=False, verbose_name='Business customer'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=fwd70, reverse_code=django.db.migrations.operations.special.RunPython.noop,
|
||||
),
|
||||
]
|
||||
53
src/pretix/base/migrations/0063_auto_20170702_1711.py
Normal file
27
src/pretix/base/migrations/0064_auto_20170703_0912.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-03 09:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.orders
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0063_auto_20170702_1711'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='questionanswer',
|
||||
name='file',
|
||||
field=models.FileField(blank=True, null=True, upload_to=pretix.base.models.orders.answerfile_name),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('N', 'Number'), ('S', 'Text (one line)'), ('T', 'Multiline text'), ('B', 'Yes/No'), ('C', 'Choose one from a list'), ('M', 'Choose multiple from a list'), ('F', 'File upload')], max_length=5, verbose_name='Question type'),
|
||||
),
|
||||
]
|
||||
30
src/pretix/base/migrations/0065_auto_20170707_0920.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-07 09:20
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0064_auto_20170703_0912'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='comment',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Internal comment'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.', null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='presale_start',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.', null=True, verbose_name='Start of presale'),
|
||||
),
|
||||
]
|
||||
103
src/pretix/base/migrations/0066_auto_20170708_2102.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-08 21:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.event
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0065_auto_20170707_0920'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SubEvent',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('active', models.BooleanField(default=False, help_text='Only with this checkbox enabled, this sub-event is visible in the frontend to users.', verbose_name='Active')),
|
||||
('name', i18nfield.fields.I18nCharField(max_length=200, verbose_name='Name')),
|
||||
('date_from', models.DateTimeField(verbose_name='Event start time')),
|
||||
('date_to', models.DateTimeField(blank=True, null=True, verbose_name='Event end time')),
|
||||
('date_admission', models.DateTimeField(blank=True, null=True, verbose_name='Admission time')),
|
||||
('presale_end', models.DateTimeField(blank=True, help_text='No products will be sold after this date.', null=True, verbose_name='End of presale')),
|
||||
('presale_start', models.DateTimeField(blank=True, help_text='No products will be sold before this date.', null=True, verbose_name='Start of presale')),
|
||||
('location', i18nfield.fields.I18nTextField(blank=True, max_length=200, null=True, verbose_name='Location')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Sub-Event',
|
||||
'verbose_name_plural': 'Sub-Events',
|
||||
'ordering': ('date_from', 'name'),
|
||||
},
|
||||
bases=(pretix.base.models.event.EventMixin, models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SubEventItem',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
|
||||
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.Item')),
|
||||
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SubEventItemVariation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=7, null=True)),
|
||||
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent')),
|
||||
('variation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pretixbase.ItemVariation')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='event',
|
||||
name='has_subevents',
|
||||
field=models.BooleanField(default=False, help_text='Only recommended for advanced users. If this feature is enabled, this will not only be a single event but a series of very similar events that are handled within a single shop. The single events inside the series can only differ in prices and quotas, not in other settings, and buying tickets across multiple of these events at the same time is possible.', verbose_name='Event series'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='event',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subevents', to='pretixbase.Event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='items',
|
||||
field=models.ManyToManyField(through='pretixbase.SubEventItem', to='pretixbase.Item'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='variations',
|
||||
field=models.ManyToManyField(through='pretixbase.SubEventItemVariation', to='pretixbase.ItemVariation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.SubEvent', verbose_name='Sub-event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='voucher',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='waitinglistentry',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Sub-event'),
|
||||
),
|
||||
]
|
||||
70
src/pretix/base/migrations/0067_auto_20170712_1610.py
Normal file
@@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-12 16:10
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0066_auto_20170708_2102'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='subevent',
|
||||
options={'ordering': ('date_from', 'name'), 'verbose_name': 'Date in event series', 'verbose_name_plural': 'Dates in event series'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='itemaddon',
|
||||
name='price_included',
|
||||
field=models.BooleanField(default=False, help_text='If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost money individually.', verbose_name='Add-Ons are included in the price'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='cartposition',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='has_subevents',
|
||||
field=models.BooleanField(default=False, verbose_name='Event series'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderposition',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='quota',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quotas', to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='active',
|
||||
field=models.BooleanField(default=False, help_text='Only with this checkbox enabled, this date is visible in the frontend to users.', verbose_name='Active'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date.', null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='presale_start',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold before this date.', null=True, verbose_name='Start of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='voucher',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='waitinglistentry',
|
||||
name='subevent',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='pretixbase.SubEvent', verbose_name='Date'),
|
||||
),
|
||||
]
|
||||
21
src/pretix/base/migrations/0068_subevent_frontpage_text.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-14 16:11
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import i18nfield.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0067_auto_20170712_1610'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='subevent',
|
||||
name='frontpage_text',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, null=True, verbose_name='Frontpage text'),
|
||||
),
|
||||
]
|
||||
47
src/pretix/base/migrations/0069_invoice_prefix.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-17 19:29
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db.models import deletion
|
||||
|
||||
|
||||
def fwd(app, schema_editor):
|
||||
Event = app.get_model('pretixbase', 'Event')
|
||||
for e in Event.objects.select_related('organizer').all():
|
||||
e.invoices.all().update(prefix=e.slug.upper() + '-', organizer=e.organizer)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0068_subevent_frontpage_text'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='prefix',
|
||||
field=models.CharField(db_index=True, default='', max_length=160),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='organizer',
|
||||
field=models.ForeignKey(null=True,
|
||||
on_delete=deletion.PROTECT,
|
||||
related_name='invoices', to='pretixbase.Organizer'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.RunPython(
|
||||
fwd, migrations.RunPython.noop
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='invoice',
|
||||
unique_together=set([('organizer', 'prefix', 'invoice_no')]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoice',
|
||||
name='organizer',
|
||||
field=models.ForeignKey(on_delete=deletion.PROTECT, related_name='invoices', to='pretixbase.Organizer'),
|
||||
),
|
||||
]
|
||||
43
src/pretix/base/migrations/0070_auto_20170719_0910.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-19 09:10
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django_countries
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def fwd(app, schema_editor):
|
||||
InvoiceAddress = app.get_model('pretixbase', 'InvoiceAddress')
|
||||
for ia in InvoiceAddress.objects.all():
|
||||
if ia.company or ia.vat_id:
|
||||
ia.is_business = True
|
||||
ia.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0069_invoice_prefix'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='invoiceaddress',
|
||||
old_name='country',
|
||||
new_name='country_old',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='country',
|
||||
field=django_countries.fields.CountryField(default='', max_length=2, verbose_name='Country'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='is_business',
|
||||
field=models.BooleanField(default=False, verbose_name='Business customer'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
fwd, migrations.RunPython.noop
|
||||
),
|
||||
]
|
||||
26
src/pretix/base/migrations/0071_auto_20170729_1616.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.3 on 2017-07-29 16:16
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import i18nfield.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0070_auto_20170719_0910'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='question',
|
||||
name='help_text',
|
||||
field=i18nfield.fields.I18nTextField(blank=True, help_text='If the question needs to be explained or clarified, do it here!', null=True, verbose_name='Help text'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceaddress',
|
||||
name='vat_id',
|
||||
field=models.CharField(blank=True, help_text='Only for business customers within the EU.', max_length=255, verbose_name='VAT ID'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-04 13:42
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0071_auto_20170729_1616'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='download_reminder_sent',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
181
src/pretix/base/migrations/0073_auto_20170716_1333.py
Normal file
@@ -0,0 +1,181 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-07-16 13:33
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
import django_countries.fields
|
||||
import i18nfield.fields
|
||||
from django.core.cache import cache
|
||||
from django.db import migrations, models
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
|
||||
def tax_rate_converter(app, schema_editor):
|
||||
EventSettingsStore = app.get_model('pretixbase', 'Event_SettingsStore')
|
||||
Item = app.get_model('pretixbase', 'Item')
|
||||
TaxRule = app.get_model('pretixbase', 'TaxRule')
|
||||
Order = app.get_model('pretixbase', 'Order')
|
||||
OrderPosition = app.get_model('pretixbase', 'OrderPosition')
|
||||
InvoiceLine = app.get_model('pretixbase', 'InvoiceLine')
|
||||
n = LazyI18nString({
|
||||
'en': 'VAT',
|
||||
'de': 'MwSt.',
|
||||
'de-informal': 'MwSt.'
|
||||
})
|
||||
|
||||
for i in Item.objects.select_related('event').exclude(tax_rate=0):
|
||||
try:
|
||||
i.tax_rule = i.event.tax_rules.get(rate=i.tax_rate)
|
||||
except TaxRule.DoesNotExist:
|
||||
tr = i.event.tax_rules.create(rate=i.tax_rate, name=n)
|
||||
i.tax_rule = tr
|
||||
i.save()
|
||||
|
||||
for o in Order.objects.select_related('event').exclude(payment_fee_tax_rate=0):
|
||||
try:
|
||||
o.payment_fee_tax_rule = o.event.tax_rules.get(rate=o.payment_fee_tax_rate)
|
||||
except TaxRule.DoesNotExist:
|
||||
tr = o.event.tax_rules.create(rate=o.payment_fee_tax_rate, name=n)
|
||||
o.tax_rule = tr
|
||||
o.save()
|
||||
|
||||
for op in OrderPosition.objects.select_related('order', 'order__event').exclude(tax_rate=0):
|
||||
try:
|
||||
op.tax_rule = op.order.event.tax_rules.get(rate=op.tax_rate)
|
||||
except TaxRule.DoesNotExist:
|
||||
tr = op.order.event.tax_rules.create(rate=op.tax_rate, name=n)
|
||||
op.tax_rule = tr
|
||||
op.save()
|
||||
|
||||
for il in InvoiceLine.objects.select_related('invoice', 'invoice__event').exclude(tax_rate=0):
|
||||
try:
|
||||
il.tax_name = il.invoice.event.tax_rules.get(rate=op.tax_rate).name
|
||||
except TaxRule.DoesNotExist:
|
||||
tr = il.invoice.event.tax_rules.create(rate=op.tax_rate, name=n)
|
||||
il.tax_name = tr.name
|
||||
il.save()
|
||||
|
||||
for setting in EventSettingsStore.objects.filter(key='tax_rate_default'):
|
||||
try:
|
||||
tr = setting.object.tax_rules.get(rate=setting.value)
|
||||
except TaxRule.DoesNotExist:
|
||||
tr = setting.object.tax_rules.create(rate=setting.value, name=n)
|
||||
setting.value = tr.pk
|
||||
setting.save()
|
||||
cache.delete('hierarkey_{}_{}'.format('event', setting.object.pk))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0072_order_download_reminder_sent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TaxRule',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', i18nfield.fields.I18nCharField(help_text='Should be short, e.g. "VAT"', max_length=190,
|
||||
verbose_name='Name')),
|
||||
('rate', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax rate')),
|
||||
('price_includes_tax', models.BooleanField(default=True,
|
||||
verbose_name='The configured product prices includes the '
|
||||
'tax amount')),
|
||||
('eu_reverse_charge',
|
||||
models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be '
|
||||
'qualified for reverse charge since the place of '
|
||||
'taxation is the location of the event. This option '
|
||||
'only enables reverse charge for business customers who '
|
||||
'entered a valid EU VAT ID. Only enable this option '
|
||||
'after consulting a tax counsel. No warranty given for '
|
||||
'correct tax calculation.',
|
||||
verbose_name='Use EU reverse charge taxation')),
|
||||
('home_country', models.CharField(blank=True,
|
||||
choices=[('AT', 'Austria'), ('BE', 'Belgium'), ('BG', 'Bulgaria'),
|
||||
('HR', 'Croatia'), ('CY', 'Cyprus'),
|
||||
('CZ', 'Czech Republic'), ('DK', 'Denmark'),
|
||||
('EE', 'Estonia'), ('FI', 'Finland'), ('FR', 'France'),
|
||||
('DE', 'Germany'), ('GR', 'Greece'), ('HU', 'Hungary'),
|
||||
('IE', 'Ireland'), ('IT', 'Italy'), ('LV', 'Latvia'),
|
||||
('LT', 'Lithuania'), ('LU', 'Luxembourg'), ('MT', 'Malta'),
|
||||
('NL', 'Netherlands'), ('PL', 'Poland'), ('PT', 'Portugal'),
|
||||
('RO', 'Romania'), ('SK', 'Slovakia'), ('SI', 'Slovenia'),
|
||||
('ES', 'Spain'), ('SE', 'Sweden'), ('UJ', 'United Kingdom')],
|
||||
help_text='Your country. Only relevant for EU reverse charge.',
|
||||
max_length=2, verbose_name='Merchant country')),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tax_rules',
|
||||
to='pretixbase.Event')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='tax_rule',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
to='pretixbase.TaxRule', verbose_name='Sales tax'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_rule',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
to='pretixbase.TaxRule'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='orderposition',
|
||||
name='tax_rule',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT,
|
||||
to='pretixbase.TaxRule'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
tax_rate_converter, migrations.RunPython.noop
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='item',
|
||||
name='tax_rate',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceaddress',
|
||||
name='vat_id_validated',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='invoiceaddress',
|
||||
name='vat_id',
|
||||
field=models.CharField(blank=True, help_text='Only for business customers within the EU.', max_length=255, verbose_name='VAT ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='taxrule',
|
||||
name='home_country',
|
||||
field=django_countries.fields.CountryField(blank=True, help_text='Your country of residence. This is the country the EU reverse charge rule will not apply in, if configured above.', max_length=2, verbose_name='Merchant country'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='includes_tax',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='tax_name',
|
||||
field=models.CharField(default='', max_length=190),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='taxrule',
|
||||
name='eu_reverse_charge',
|
||||
field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries that do not customers who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='foreign_currency_display',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='foreign_currency_rate',
|
||||
field=models.DecimalField(blank=True, decimal_places=4, max_digits=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='invoice',
|
||||
name='foreign_currency_rate_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
38
src/pretix/base/migrations/0074_auto_20170825_1258.py
Normal file
60
src/pretix/base/migrations/0075_auto_20170828_0901.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-28 09:01
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.base
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0074_auto_20170825_1258'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EventMetaProperty',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(db_index=True, help_text='Can not contain spaces or special characters execpt underscores', max_length=50, validators=[django.core.validators.RegexValidator(message='The property name may only contain letters, numbers and underscores.', regex='^[a-zA-Z0-9_]+$')], verbose_name='Name')),
|
||||
('default', models.TextField()),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_properties', to='pretixbase.Organizer')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EventMetaValue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.TextField()),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.Event')),
|
||||
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_values', to='pretixbase.EventMetaProperty')),
|
||||
],
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SubEventMetaValue',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.TextField()),
|
||||
('property', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subevent_values', to='pretixbase.EventMetaProperty')),
|
||||
('subevent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meta_values', to='pretixbase.SubEvent')),
|
||||
],
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='subeventmetavalue',
|
||||
unique_together=set([('subevent', 'property')]),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='eventmetavalue',
|
||||
unique_together=set([('event', 'property')]),
|
||||
),
|
||||
]
|
||||
72
src/pretix/base/migrations/0076_orderfee.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-28 14:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def fee_converter(app, schema_editor):
|
||||
OrderFee = app.get_model('pretixbase', 'OrderFee')
|
||||
Order = app.get_model('pretixbase', 'Order')
|
||||
|
||||
of = []
|
||||
for o in Order.objects.exclude(payment_fee=Decimal('0.00')).iterator():
|
||||
of.append(OrderFee(
|
||||
order=o,
|
||||
value=o.payment_fee,
|
||||
fee_type='payment',
|
||||
tax_rate=o.payment_fee_tax_rate,
|
||||
tax_rule=o.payment_fee_tax_rule,
|
||||
tax_value=o.payment_fee_tax_value,
|
||||
internal_type=o.payment_provider
|
||||
))
|
||||
if len(of) > 900:
|
||||
OrderFee.objects.bulk_create(of)
|
||||
of = []
|
||||
OrderFee.objects.bulk_create(of)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0075_auto_20170828_0901'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrderFee',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Value')),
|
||||
('description', models.CharField(blank=True, max_length=190)),
|
||||
('internal_type', models.CharField(blank=True, max_length=255)),
|
||||
('fee_type', models.CharField(choices=[('payment', 'Payment method fee'), ('shipping', 'Shipping fee')], max_length=100)),
|
||||
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Tax rate')),
|
||||
('tax_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax value')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='fees', to='pretixbase.Order', verbose_name='Order')),
|
||||
('tax_rule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.TaxRule')),
|
||||
],
|
||||
),
|
||||
migrations.RunPython(
|
||||
fee_converter, migrations.RunPython.noop
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_rate',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_rule',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='payment_fee_tax_value',
|
||||
),
|
||||
]
|
||||
41
src/pretix/base/migrations/0077_auto_20170829_1126.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.4 on 2017-08-29 11:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def assign_positions(app, schema_editor):
|
||||
Invoice = app.get_model('pretixbase', 'Invoice')
|
||||
|
||||
for i in Invoice.objects.iterator():
|
||||
for j, l in enumerate(i.lines.all()):
|
||||
l.position = j
|
||||
l.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0076_orderfee'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='invoiceline',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='fee_type',
|
||||
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('other', 'Other fees')], max_length=100),
|
||||
),
|
||||
migrations.RunPython(
|
||||
assign_positions, migrations.RunPython.noop
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='invoiceline',
|
||||
options={'ordering': ('position', 'pk')},
|
||||
),
|
||||
]
|
||||
40
src/pretix/base/migrations/0078_auto_20171003_1650.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.5 on 2017-10-03 16:50
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0077_auto_20170829_1126'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_number',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_state',
|
||||
field=models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='quota',
|
||||
name='cached_availability_time',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='eventmetaproperty',
|
||||
name='default',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='taxrule',
|
||||
name='eu_reverse_charge',
|
||||
field=models.BooleanField(default=False, help_text='Not recommended. Most events will NOT be qualified for reverse charge since the place of taxation is the location of the event. This option disables charging VAT for all customers outside the EU and for business customers in different EU countries who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. No warranty given for correct tax calculation. USE AT YOUR OWN RISK.', verbose_name='Use EU reverse charge taxation rules'),
|
||||
),
|
||||
]
|
||||
@@ -3,13 +3,13 @@ from .auth import U2FDevice, User
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, RequiredAction,
|
||||
Event, Event_SettingsStore, EventLock, RequiredAction, SubEvent,
|
||||
generate_invite_token,
|
||||
)
|
||||
from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
Item, ItemAddOn, ItemCategory, ItemVariation, Question, QuestionOption,
|
||||
Quota, itempicture_upload_to,
|
||||
Quota, SubEventItem, SubEventItemVariation, itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .orders import (
|
||||
@@ -19,5 +19,6 @@ from .orders import (
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
|
||||
from .tax import TaxRule
|
||||
from .vouchers import Voucher
|
||||
from .waitinglist import WaitingListEntry
|
||||
|
||||
@@ -6,7 +6,8 @@ from django.db import models
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils.crypto import get_random_string
|
||||
from i18nfield.utils import I18nJSONEncoder
|
||||
|
||||
from pretix.helpers.json import CustomJSONEncoder
|
||||
|
||||
|
||||
def cachedfile_name(instance, filename: str) -> str:
|
||||
@@ -16,7 +17,7 @@ def cachedfile_name(instance, filename: str) -> str:
|
||||
|
||||
class CachedFile(models.Model):
|
||||
"""
|
||||
A cached file (e.g. pre-generated ticket PDF)
|
||||
An uploaded file, with an optional expiry date.
|
||||
"""
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
|
||||
expires = models.DateTimeField(null=True, blank=True)
|
||||
@@ -54,7 +55,7 @@ class LoggingMixin:
|
||||
event = self.event
|
||||
l = LogEntry(content_object=self, user=user, action_type=action, event=event)
|
||||
if data:
|
||||
l.data = json.dumps(data, cls=I18nJSONEncoder)
|
||||
l.data = json.dumps(data, cls=CustomJSONEncoder)
|
||||
l.save()
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import string
|
||||
import uuid
|
||||
from datetime import date, datetime, time
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime, time
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
@@ -9,122 +10,26 @@ from django.core.files.storage import default_storage
|
||||
from django.core.mail import get_connection
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.email import CustomSMTPBackend
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.validators import EventSlugBlacklistValidator
|
||||
from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.json import safe_string
|
||||
|
||||
from ..settings import settings_hierarkey
|
||||
from .organizer import Organizer
|
||||
|
||||
|
||||
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
|
||||
class Event(LoggedModel):
|
||||
"""
|
||||
This model represents an event. An event is anything you can buy
|
||||
tickets for.
|
||||
|
||||
:param organizer: The organizer this event belongs to
|
||||
:type organizer: Organizer
|
||||
:param name: This event's full title
|
||||
:type name: str
|
||||
:param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
|
||||
be unique among the events of the same organizer.
|
||||
:type slug: str
|
||||
:param live: Whether or not the shop is publicly accessible
|
||||
:type live: bool
|
||||
:param currency: The currency of all prices and payments of this event
|
||||
:type currency: str
|
||||
:param date_from: The datetime this event starts
|
||||
:type date_from: datetime
|
||||
:param date_to: The datetime this event ends
|
||||
:type date_to: datetime
|
||||
:param presale_start: No tickets will be sold before this date.
|
||||
:type presale_start: datetime
|
||||
:param presale_end: No tickets will be sold after this date.
|
||||
:type presale_end: datetime
|
||||
:param location: venue
|
||||
:type location: str
|
||||
:param plugins: A comma-separated list of plugin names that are active for this
|
||||
event.
|
||||
:type plugins: str
|
||||
"""
|
||||
|
||||
settings_namespace = 'event'
|
||||
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
|
||||
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
|
||||
name = I18nCharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=50, db_index=True,
|
||||
help_text=_(
|
||||
"Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
|
||||
"We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
|
||||
"remembered, but you can also choose to use a random value. "
|
||||
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex="^[a-zA-Z0-9.-]+$",
|
||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||
),
|
||||
EventSlugBlacklistValidator()
|
||||
],
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
|
||||
currency = models.CharField(max_length=10,
|
||||
verbose_name=_("Default currency"),
|
||||
choices=CURRENCY_CHOICES,
|
||||
default=settings.DEFAULT_CURRENCY)
|
||||
date_from = models.DateTimeField(verbose_name=_("Event start time"))
|
||||
date_to = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Event end time"))
|
||||
date_admission = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Admission time"))
|
||||
is_public = models.BooleanField(default=False,
|
||||
verbose_name=_("Visible in public lists"),
|
||||
help_text=_("If selected, this event may show up on the ticket system's start page "
|
||||
"or an organization profile."))
|
||||
presale_end = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("End of presale"),
|
||||
help_text=_("No products will be sold after this date."),
|
||||
)
|
||||
presale_start = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Start of presale"),
|
||||
help_text=_("No products will be sold before this date."),
|
||||
)
|
||||
location = I18nTextField(
|
||||
null=True, blank=True,
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
plugins = models.TextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Plugins"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Event")
|
||||
verbose_name_plural = _("Events")
|
||||
ordering = ("date_from", "name")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
obj = super().save(*args, **kwargs)
|
||||
self.get_cache().clear()
|
||||
return obj
|
||||
class EventMixin:
|
||||
|
||||
def clean(self):
|
||||
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
|
||||
@@ -133,13 +38,30 @@ class Event(LoggedModel):
|
||||
raise ValidationError({'date_to': _('The end of the event has to be later than its start.')})
|
||||
super().clean()
|
||||
|
||||
def get_plugins(self) -> "list[str]":
|
||||
def get_short_date_from_display(self, tz=None, show_times=True) -> str:
|
||||
"""
|
||||
Returns the names of the plugins activated for this event as a list.
|
||||
Returns a shorter formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
"""
|
||||
if self.plugins is None:
|
||||
return []
|
||||
return self.plugins.split(",")
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
)
|
||||
|
||||
def get_short_date_to_display(self, tz=None) -> str:
|
||||
"""
|
||||
Returns a shorter formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting. Returns an empty string
|
||||
if ``show_date_to`` is ``False``.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return ""
|
||||
return _date(
|
||||
self.date_to.astimezone(tz),
|
||||
"SHORT_DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT"
|
||||
)
|
||||
|
||||
def get_date_from_display(self, tz=None, show_times=True) -> str:
|
||||
"""
|
||||
@@ -177,11 +99,182 @@ class Event(LoggedModel):
|
||||
)
|
||||
|
||||
def get_date_range_display(self, tz=None) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date and the event date
|
||||
of the event with respect to the current locale and to the ``show_times`` and
|
||||
``show_date_to`` settings.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return _date(self.date_from.astimezone(tz), "DATE_FORMAT")
|
||||
return daterange(self.date_from.astimezone(tz), self.date_to.astimezone(tz))
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
"""
|
||||
Is true, when ``presale_end`` is set and in the past.
|
||||
"""
|
||||
if self.presale_end and now() > self.presale_end:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def presale_is_running(self):
|
||||
"""
|
||||
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
|
||||
set or in the past.
|
||||
"""
|
||||
if self.presale_start and now() < self.presale_start:
|
||||
return False
|
||||
if self.presale_end and now() > self.presale_end:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def event_microdata(self):
|
||||
import json
|
||||
|
||||
eventdict = {
|
||||
"@context": "http://schema.org",
|
||||
"@type": "Event", "location": {
|
||||
"@type": "Place",
|
||||
"address": str(self.location)
|
||||
},
|
||||
"name": str(self.name)
|
||||
}
|
||||
|
||||
if self.settings.show_times:
|
||||
eventdict["startDate"] = self.date_from.isoformat()
|
||||
if self.settings.show_date_to and self.date_to is not None:
|
||||
eventdict["endDate"] = self.date_to.isoformat()
|
||||
else:
|
||||
eventdict["startDate"] = self.date_from.date().isoformat()
|
||||
if self.settings.show_date_to and self.date_to is not None:
|
||||
eventdict["endDate"] = self.date_to.date().isoformat()
|
||||
|
||||
return safe_string(json.dumps(eventdict))
|
||||
|
||||
|
||||
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
|
||||
class Event(EventMixin, LoggedModel):
|
||||
"""
|
||||
This model represents an event. An event is anything you can buy
|
||||
tickets for.
|
||||
|
||||
:param organizer: The organizer this event belongs to
|
||||
:type organizer: Organizer
|
||||
:param name: This event's full title
|
||||
:type name: str
|
||||
:param slug: A short, alphanumeric, all-lowercase name for use in URLs. The slug has to
|
||||
be unique among the events of the same organizer.
|
||||
:type slug: str
|
||||
:param live: Whether or not the shop is publicly accessible
|
||||
:type live: bool
|
||||
:param currency: The currency of all prices and payments of this event
|
||||
:type currency: str
|
||||
:param date_from: The datetime this event starts
|
||||
:type date_from: datetime
|
||||
:param date_to: The datetime this event ends
|
||||
:type date_to: datetime
|
||||
:param presale_start: No tickets will be sold before this date.
|
||||
:type presale_start: datetime
|
||||
:param presale_end: No tickets will be sold after this date.
|
||||
:type presale_end: datetime
|
||||
:param location: venue
|
||||
:type location: str
|
||||
:param plugins: A comma-separated list of plugin names that are active for this
|
||||
event.
|
||||
:type plugins: str
|
||||
:param has_subevents: Enable event series functionality
|
||||
:type has_subevents: bool
|
||||
"""
|
||||
|
||||
settings_namespace = 'event'
|
||||
CURRENCY_CHOICES = [(c.alpha_3, c.alpha_3 + " - " + c.name) for c in settings.CURRENCIES]
|
||||
organizer = models.ForeignKey(Organizer, related_name="events", on_delete=models.PROTECT)
|
||||
name = I18nCharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Event name"),
|
||||
)
|
||||
slug = models.SlugField(
|
||||
max_length=50, db_index=True,
|
||||
help_text=_(
|
||||
"Should be short, only contain lowercase letters and numbers, and must be unique among your events. "
|
||||
"We recommend some kind of abbreviation or a date with less than 10 characters that can be easily "
|
||||
"remembered, but you can also choose to use a random value. "
|
||||
"This will be used in URLs, order codes, invoice numbers, and bank transfer references."),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex="^[a-zA-Z0-9.-]+$",
|
||||
message=_("The slug may only contain letters, numbers, dots and dashes."),
|
||||
),
|
||||
EventSlugBlacklistValidator()
|
||||
],
|
||||
verbose_name=_("Short form"),
|
||||
)
|
||||
live = models.BooleanField(default=False, verbose_name=_("Shop is live"))
|
||||
currency = models.CharField(max_length=10,
|
||||
verbose_name=_("Event currency"),
|
||||
choices=CURRENCY_CHOICES,
|
||||
default=settings.DEFAULT_CURRENCY)
|
||||
date_from = models.DateTimeField(verbose_name=_("Event start time"))
|
||||
date_to = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Event end time"))
|
||||
date_admission = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Admission time"))
|
||||
is_public = models.BooleanField(default=False,
|
||||
verbose_name=_("Visible in public lists"),
|
||||
help_text=_("If selected, this event may show up on the ticket system's start page "
|
||||
"or an organization profile."))
|
||||
presale_end = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("End of presale"),
|
||||
help_text=_("Optional. No products will be sold after this date."),
|
||||
)
|
||||
presale_start = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Start of presale"),
|
||||
help_text=_("Optional. No products will be sold before this date."),
|
||||
)
|
||||
location = I18nTextField(
|
||||
null=True, blank=True,
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
plugins = models.TextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Plugins"),
|
||||
)
|
||||
comment = models.TextField(
|
||||
verbose_name=_("Internal comment"),
|
||||
null=True, blank=True
|
||||
)
|
||||
has_subevents = models.BooleanField(
|
||||
verbose_name=_('Event series'),
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Event")
|
||||
verbose_name_plural = _("Events")
|
||||
ordering = ("date_from", "name")
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
obj = super().save(*args, **kwargs)
|
||||
self.get_cache().clear()
|
||||
return obj
|
||||
|
||||
def get_plugins(self) -> "list[str]":
|
||||
"""
|
||||
Returns the names of the plugins activated for this event as a list.
|
||||
"""
|
||||
if self.plugins is None:
|
||||
return []
|
||||
return self.plugins.split(",")
|
||||
|
||||
def get_cache(self) -> "pretix.base.cache.ObjectRelatedCache":
|
||||
"""
|
||||
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
|
||||
@@ -193,20 +286,6 @@ class Event(LoggedModel):
|
||||
|
||||
return ObjectRelatedCache(self)
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
if self.presale_end and now() > self.presale_end:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def presale_is_running(self):
|
||||
if self.presale_start and now() < self.presale_start:
|
||||
return False
|
||||
if self.presale_end and now() > self.presale_end:
|
||||
return False
|
||||
return True
|
||||
|
||||
def lock(self):
|
||||
"""
|
||||
Returns a contextmanager that can be used to lock an event for bookings.
|
||||
@@ -216,6 +295,10 @@ class Event(LoggedModel):
|
||||
return locking.LockManager(self)
|
||||
|
||||
def get_mail_backend(self, force_custom=False):
|
||||
"""
|
||||
Returns an email server connection, either by using the system-wide connection
|
||||
or by returning a custom one based on the event's settings.
|
||||
"""
|
||||
if self.settings.smtp_use_custom or force_custom:
|
||||
return CustomSMTPBackend(host=self.settings.smtp_host,
|
||||
port=self.settings.smtp_port,
|
||||
@@ -229,9 +312,12 @@ class Event(LoggedModel):
|
||||
|
||||
@property
|
||||
def payment_term_last(self):
|
||||
"""
|
||||
The last datetime of payments for this event.
|
||||
"""
|
||||
tz = pytz.timezone(self.settings.timezone)
|
||||
return make_aware(datetime.combine(
|
||||
self.settings.get('payment_term_last', as_type=date),
|
||||
self.settings.get('payment_term_last', as_type=RelativeDateWrapper).datetime(self).date(),
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
|
||||
@@ -240,8 +326,16 @@ class Event(LoggedModel):
|
||||
from ..signals import event_copy_data
|
||||
|
||||
self.plugins = other.plugins
|
||||
self.is_public = other.is_public
|
||||
self.save()
|
||||
|
||||
tax_map = {}
|
||||
for t in other.tax_rules.all():
|
||||
tax_map[t.pk] = t
|
||||
t.pk = None
|
||||
t.event = self
|
||||
t.save()
|
||||
|
||||
category_map = {}
|
||||
for c in ItemCategory.objects.filter(event=other):
|
||||
category_map[c.pk] = c
|
||||
@@ -260,6 +354,8 @@ class Event(LoggedModel):
|
||||
i.picture.save(i.picture.name, i.picture)
|
||||
if i.category_id:
|
||||
i.category = category_map[i.category_id]
|
||||
if i.tax_rule_id:
|
||||
i.tax_rule = tax_map[i.tax_rule_id]
|
||||
i.save()
|
||||
for v in vars:
|
||||
variation_map[v.pk] = v
|
||||
@@ -273,7 +369,7 @@ class Event(LoggedModel):
|
||||
ia.addon_category = category_map[ia.addon_category.pk]
|
||||
ia.save()
|
||||
|
||||
for q in Quota.objects.filter(event=other).prefetch_related('items', 'variations'):
|
||||
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
|
||||
items = list(q.items.all())
|
||||
vars = list(q.variations.all())
|
||||
q.pk = None
|
||||
@@ -309,11 +405,25 @@ class Event(LoggedModel):
|
||||
)
|
||||
newname = default_storage.save(fname, fi)
|
||||
s.value = 'file://' + newname
|
||||
s.save()
|
||||
s.save()
|
||||
elif s.key == 'tax_rate_default':
|
||||
try:
|
||||
if int(s.value) in tax_map:
|
||||
s.value = tax_map.get(int(s.value)).pk
|
||||
s.save()
|
||||
else:
|
||||
s.delete()
|
||||
except ValueError:
|
||||
s.delete()
|
||||
else:
|
||||
s.save()
|
||||
|
||||
event_copy_data.send(sender=self, other=other)
|
||||
|
||||
def get_payment_providers(self) -> dict:
|
||||
"""
|
||||
Returns a dictionary of initialized payment providers mapped by their identifiers.
|
||||
"""
|
||||
from ..signals import register_payment_providers
|
||||
|
||||
responses = register_payment_providers.send(self)
|
||||
@@ -324,7 +434,149 @@ class Event(LoggedModel):
|
||||
for p in response:
|
||||
pp = p(self)
|
||||
providers[pp.identifier] = pp
|
||||
return providers
|
||||
|
||||
return OrderedDict(sorted(providers.items(), key=lambda v: str(v[1].verbose_name)))
|
||||
|
||||
def get_invoice_renderers(self) -> dict:
|
||||
"""
|
||||
Returns a dictionary of initialized invoice renderers mapped by their identifiers.
|
||||
"""
|
||||
from ..signals import register_invoice_renderers
|
||||
|
||||
responses = register_invoice_renderers.send(self)
|
||||
renderers = {}
|
||||
for receiver, response in responses:
|
||||
if not isinstance(response, list):
|
||||
response = [response]
|
||||
for p in response:
|
||||
pp = p(self)
|
||||
renderers[pp.identifier] = pp
|
||||
return renderers
|
||||
|
||||
@property
|
||||
def invoice_renderer(self):
|
||||
"""
|
||||
Returns the currently configured invoice renderer.
|
||||
"""
|
||||
irs = self.get_invoice_renderers()
|
||||
return irs[self.settings.invoice_renderer]
|
||||
|
||||
@property
|
||||
def active_subevents(self):
|
||||
"""
|
||||
Returns a queryset of active subevents.
|
||||
"""
|
||||
return self.subevents.filter(active=True).order_by('-date_from', 'name')
|
||||
|
||||
@property
|
||||
def active_future_subevents(self):
|
||||
return self.subevents.filter(
|
||||
Q(active=True) & (
|
||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
|
||||
| Q(date_to__gte=now())
|
||||
)
|
||||
).order_by('date_from', 'name')
|
||||
|
||||
@property
|
||||
def meta_data(self):
|
||||
data = {p.name: p.default for p in self.organizer.meta_properties.all()}
|
||||
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||
return data
|
||||
|
||||
|
||||
class SubEvent(EventMixin, LoggedModel):
|
||||
"""
|
||||
This model represents a date within an event series.
|
||||
|
||||
:param event: The event this belongs to
|
||||
:type event: Event
|
||||
:param active: Whether to show the subevent
|
||||
:type active: bool
|
||||
:param name: This event's full title
|
||||
:type name: str
|
||||
:param date_from: The datetime this event starts
|
||||
:type date_from: datetime
|
||||
:param date_to: The datetime this event ends
|
||||
:type date_to: datetime
|
||||
:param presale_start: No tickets will be sold before this date.
|
||||
:type presale_start: datetime
|
||||
:param presale_end: No tickets will be sold after this date.
|
||||
:type presale_end: datetime
|
||||
:param location: venue
|
||||
:type location: str
|
||||
"""
|
||||
|
||||
event = models.ForeignKey(Event, related_name="subevents", on_delete=models.PROTECT)
|
||||
active = models.BooleanField(default=False, verbose_name=_("Active"),
|
||||
help_text=_("Only with this checkbox enabled, this date is visible in the "
|
||||
"frontend to users."))
|
||||
name = I18nCharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
date_from = models.DateTimeField(verbose_name=_("Event start time"))
|
||||
date_to = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Event end time"))
|
||||
date_admission = models.DateTimeField(null=True, blank=True,
|
||||
verbose_name=_("Admission time"))
|
||||
presale_end = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("End of presale"),
|
||||
help_text=_("Optional. No products will be sold after this date."),
|
||||
)
|
||||
presale_start = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Start of presale"),
|
||||
help_text=_("Optional. No products will be sold before this date."),
|
||||
)
|
||||
location = I18nTextField(
|
||||
null=True, blank=True,
|
||||
max_length=200,
|
||||
verbose_name=_("Location"),
|
||||
)
|
||||
frontpage_text = I18nTextField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("Frontpage text")
|
||||
)
|
||||
|
||||
items = models.ManyToManyField('Item', through='SubEventItem')
|
||||
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Date in event series")
|
||||
verbose_name_plural = _("Dates in event series")
|
||||
ordering = ("date_from", "name")
|
||||
|
||||
def __str__(self):
|
||||
return '{} - {}'.format(self.name, self.get_date_range_display())
|
||||
|
||||
@cached_property
|
||||
def settings(self):
|
||||
return self.event.settings
|
||||
|
||||
@cached_property
|
||||
def item_price_overrides(self):
|
||||
from .items import SubEventItem
|
||||
|
||||
return {
|
||||
si.item_id: si.price
|
||||
for si in SubEventItem.objects.filter(subevent=self, price__isnull=False)
|
||||
}
|
||||
|
||||
@cached_property
|
||||
def var_price_overrides(self):
|
||||
from .items import SubEventItemVariation
|
||||
|
||||
return {
|
||||
si.variation_id: si.price
|
||||
for si in SubEventItemVariation.objects.filter(subevent=self, price__isnull=False)
|
||||
}
|
||||
|
||||
@property
|
||||
def meta_data(self):
|
||||
data = self.event.meta_data
|
||||
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||
return data
|
||||
|
||||
|
||||
def generate_invite_token():
|
||||
@@ -374,3 +626,74 @@ class RequiredAction(models.Model):
|
||||
if response:
|
||||
return response
|
||||
return self.action_type
|
||||
|
||||
|
||||
class EventMetaProperty(LoggedModel):
|
||||
"""
|
||||
An organizer account can have EventMetaProperty objects attached to define meta information fields
|
||||
for its events. This information can be re-used for example in ticket layouts.
|
||||
|
||||
:param organizer: The organizer this property is defined for.
|
||||
:type organizer: Organizer
|
||||
:param name: Name
|
||||
:type name: Name of the property, used in various places
|
||||
:param default: Default value
|
||||
:type default: str
|
||||
"""
|
||||
organizer = models.ForeignKey(Organizer, related_name="meta_properties", on_delete=models.CASCADE)
|
||||
name = models.CharField(
|
||||
max_length=50, db_index=True,
|
||||
help_text=_(
|
||||
"Can not contain spaces or special characters execpt underscores"
|
||||
),
|
||||
validators=[
|
||||
RegexValidator(
|
||||
regex="^[a-zA-Z0-9_]+$",
|
||||
message=_("The property name may only contain letters, numbers and underscores."),
|
||||
),
|
||||
],
|
||||
verbose_name=_("Name"),
|
||||
)
|
||||
default = models.TextField(blank=True)
|
||||
|
||||
|
||||
class EventMetaValue(LoggedModel):
|
||||
"""
|
||||
A meta-data value assigned to an event.
|
||||
|
||||
:param event: The event this metadata is valid for
|
||||
:type event: Event
|
||||
:param property: The property this value belongs to
|
||||
:type property: EventMetaProperty
|
||||
:param value: The actual value
|
||||
:type value: str
|
||||
"""
|
||||
event = models.ForeignKey('Event', on_delete=models.CASCADE,
|
||||
related_name='meta_values')
|
||||
property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE,
|
||||
related_name='event_values')
|
||||
value = models.TextField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('event', 'property')
|
||||
|
||||
|
||||
class SubEventMetaValue(LoggedModel):
|
||||
"""
|
||||
A meta-data value assigned to a sub-event.
|
||||
|
||||
:param event: The event this metadata is valid for
|
||||
:type event: Event
|
||||
:param property: The property this value belongs to
|
||||
:type property: EventMetaProperty
|
||||
:param value: The actual value
|
||||
:type value: str
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE,
|
||||
related_name='meta_values')
|
||||
property = models.ForeignKey('EventMetaProperty', on_delete=models.CASCADE,
|
||||
related_name='subevent_values')
|
||||
value = models.TextField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('subevent', 'property')
|
||||
|
||||
@@ -9,9 +9,10 @@ from django.utils.functional import cached_property
|
||||
|
||||
def invoice_filename(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=16, allowed_chars=string.ascii_letters + string.digits)
|
||||
return 'invoices/{org}/{ev}/{no}-{code}-{secret}.pdf'.format(
|
||||
return 'invoices/{org}/{ev}/{no}-{code}-{secret}.{ext}'.format(
|
||||
org=instance.event.organizer.slug, ev=instance.event.slug,
|
||||
no=instance.number, code=instance.order.code, secret=secret
|
||||
no=instance.number, code=instance.order.code, secret=secret,
|
||||
ext=filename.split('.')[-1]
|
||||
)
|
||||
|
||||
|
||||
@@ -28,6 +29,8 @@ class Invoice(models.Model):
|
||||
:type order: Order
|
||||
:param event: The event this belongs to (for convenience)
|
||||
:type event: Event
|
||||
:param organizer: The organizer this belongs to (redundant, for enforcing uniqueness)
|
||||
:type organizer: Organizer
|
||||
:param invoice_no: The human-readable, event-unique invoice number
|
||||
:type invoice_no: int
|
||||
:param is_cancellation: Whether or not this is a cancellation instead of an invoice
|
||||
@@ -50,11 +53,19 @@ class Invoice(models.Model):
|
||||
:type payment_provider_text: str
|
||||
:param footer_text: A footer text, displayed smaller and centered on every page
|
||||
:type footer_text: str
|
||||
:param foreign_currency_display: A different currency that taxes should also be displayed in.
|
||||
:type foreign_currency_display: str
|
||||
:param foreign_currency_rate: The rate of a forein currency that the taxes should be displayed in.
|
||||
:type foreign_currency_rate: Decimal
|
||||
:param foreign_currency_rate_date: The date of the forein currency exchange rates.
|
||||
:type foreign_currency_rate_date: date
|
||||
:param file: The filename of the rendered invoice
|
||||
:type file: File
|
||||
"""
|
||||
order = models.ForeignKey('Order', related_name='invoices', db_index=True)
|
||||
organizer = models.ForeignKey('Organizer', related_name='invoices', db_index=True, on_delete=models.PROTECT)
|
||||
event = models.ForeignKey('Event', related_name='invoices', db_index=True)
|
||||
prefix = models.CharField(max_length=160, db_index=True)
|
||||
invoice_no = models.CharField(max_length=19, db_index=True)
|
||||
is_cancellation = models.BooleanField(default=False)
|
||||
refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True)
|
||||
@@ -66,6 +77,9 @@ class Invoice(models.Model):
|
||||
additional_text = models.TextField(blank=True)
|
||||
payment_provider_text = models.TextField(blank=True)
|
||||
footer_text = models.TextField(blank=True)
|
||||
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
|
||||
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
|
||||
foreign_currency_rate_date = models.DateField(null=True, blank=True)
|
||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
|
||||
|
||||
@staticmethod
|
||||
@@ -73,7 +87,10 @@ class Invoice(models.Model):
|
||||
return '{:05d}'.format(int(number))
|
||||
|
||||
def _get_numeric_invoice_number(self):
|
||||
numeric_invoices = Invoice.objects.filter(event=self.event).exclude(invoice_no__contains='-')
|
||||
numeric_invoices = Invoice.objects.filter(
|
||||
event__organizer=self.event.organizer,
|
||||
prefix=self.prefix,
|
||||
).exclude(invoice_no__contains='-')
|
||||
return self._to_numeric_invoice_number(numeric_invoices.count() + 1)
|
||||
|
||||
def _get_invoice_number_from_order(self):
|
||||
@@ -87,6 +104,10 @@ class Invoice(models.Model):
|
||||
raise ValueError('Every invoice needs to be connected to an order')
|
||||
if not self.event:
|
||||
self.event = self.order.event
|
||||
if not self.organizer:
|
||||
self.organizer = self.order.event.organizer
|
||||
if not self.prefix:
|
||||
self.prefix = self.event.settings.invoice_numbers_prefix or (self.event.slug.upper() + '-')
|
||||
if not self.invoice_no:
|
||||
for i in range(10):
|
||||
if self.event.settings.get('invoice_numbers_consecutive'):
|
||||
@@ -115,8 +136,8 @@ class Invoice(models.Model):
|
||||
"""
|
||||
Returns the invoice number in a human-readable string with the event slug prepended.
|
||||
"""
|
||||
return '{event}-{code}'.format(
|
||||
event=self.event.slug.upper(),
|
||||
return '{prefix}{code}'.format(
|
||||
prefix=self.prefix,
|
||||
code=self.invoice_no
|
||||
)
|
||||
|
||||
@@ -125,7 +146,7 @@ class Invoice(models.Model):
|
||||
return self.refered.filter(is_cancellation=True).exists()
|
||||
|
||||
class Meta:
|
||||
unique_together = ('event', 'invoice_no')
|
||||
unique_together = ('organizer', 'prefix', 'invoice_no')
|
||||
ordering = ('invoice_no',)
|
||||
|
||||
|
||||
@@ -143,13 +164,20 @@ class InvoiceLine(models.Model):
|
||||
:type tax_value: decimal.Decimal
|
||||
:param tax_rate: The applied tax rate in percent
|
||||
:type tax_rate: decimal.Decimal
|
||||
:param tax_name: The name of the applied tax rate
|
||||
:type tax_name: str
|
||||
"""
|
||||
invoice = models.ForeignKey('Invoice', related_name='lines')
|
||||
position = models.PositiveIntegerField(default=0)
|
||||
description = models.TextField()
|
||||
gross_value = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_name = models.CharField(max_length=190)
|
||||
|
||||
@property
|
||||
def net_value(self):
|
||||
return self.gross_value - self.tax_value
|
||||
|
||||
class Meta:
|
||||
ordering = ('position', 'pk')
|
||||
|
||||
@@ -10,13 +10,13 @@ from django.db import models
|
||||
from django.db.models import F, Func, Q, Sum
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
|
||||
from .event import Event
|
||||
from .event import Event, SubEvent
|
||||
|
||||
|
||||
class ItemCategory(LoggedModel):
|
||||
@@ -88,6 +88,40 @@ def itempicture_upload_to(instance, filename: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
class SubEventItem(models.Model):
|
||||
"""
|
||||
This model can be used to change the price of a product for a single subevent (i.e. a
|
||||
date in an event series).
|
||||
|
||||
:param subevent: The date this belongs to
|
||||
:type subevent: SubEvent
|
||||
:param item: The item to modify the price for
|
||||
:type item: Item
|
||||
:param price: The modified price (or ``None`` for the original price)
|
||||
:type price: Decimal
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
item = models.ForeignKey('Item', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
|
||||
|
||||
class SubEventItemVariation(models.Model):
|
||||
"""
|
||||
This model can be used to change the price of a product variation for a single
|
||||
subevent (i.e. a date in an event series).
|
||||
|
||||
:param subevent: The date this belongs to
|
||||
:type subevent: SubEvent
|
||||
:param variation: The variation to modify the price for
|
||||
:type variation: ItemVariation
|
||||
:param price: The modified price (or ``None`` for the original price)
|
||||
:type price: Decimal
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
|
||||
|
||||
class Item(LoggedModel):
|
||||
"""
|
||||
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
|
||||
@@ -125,6 +159,8 @@ class Item(LoggedModel):
|
||||
:type max_per_order: int
|
||||
:param min_per_order: Minimum number of times this item needs to be in an order if bought at all. None for unlimited.
|
||||
:type min_per_order: int
|
||||
:param checkin_attention: Requires special attention at checkin
|
||||
:type checkin_attention: bool
|
||||
"""
|
||||
|
||||
event = models.ForeignKey(
|
||||
@@ -139,6 +175,7 @@ class Item(LoggedModel):
|
||||
related_name="items",
|
||||
blank=True, null=True,
|
||||
verbose_name=_("Category"),
|
||||
help_text=_("If you have many products, you can optionally sort them into categories to keep things organized.")
|
||||
)
|
||||
name = I18nCharField(
|
||||
max_length=255,
|
||||
@@ -168,10 +205,11 @@ class Item(LoggedModel):
|
||||
"additional donations for your event. This is currently not supported for products that are "
|
||||
"bought as an add-on to other products.")
|
||||
)
|
||||
tax_rate = models.DecimalField(
|
||||
verbose_name=_("Taxes included in percent"),
|
||||
max_digits=7, decimal_places=2,
|
||||
default=Decimal('0.00')
|
||||
tax_rule = models.ForeignKey(
|
||||
'TaxRule',
|
||||
verbose_name=_('Sales tax'),
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
admission = models.BooleanField(
|
||||
verbose_name=_("Is an admission ticket"),
|
||||
@@ -231,6 +269,13 @@ class Item(LoggedModel):
|
||||
'empty or set it to 0, there is no special limit for this product. The limit for the maximum '
|
||||
'number of items in the whole order applies regardless.')
|
||||
)
|
||||
checkin_attention = models.BooleanField(
|
||||
verbose_name=_('Requires special attention'),
|
||||
default=False,
|
||||
help_text=_('If you set this, the check-in app will show a visible warning that this ticket requires special '
|
||||
'attention. You can use this for example for student tickets to indicate to the person at '
|
||||
'check-in that the student ID card still needs to be checked.')
|
||||
)
|
||||
# !!! Attention: If you add new fields here, also add them to the copying code in
|
||||
# pretix/control/views/item.py if applicable.
|
||||
|
||||
@@ -252,10 +297,12 @@ class Item(LoggedModel):
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
@property
|
||||
def default_price_net(self):
|
||||
tax_value = round_decimal(self.default_price * (1 - 100 / (100 + self.tax_rate)))
|
||||
return self.default_price - tax_value
|
||||
def tax(self, price=None, base_price_is='auto'):
|
||||
price = price if price is not None else self.default_price
|
||||
if not self.tax_rule:
|
||||
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
return self.tax_rule.tax(price, base_price_is=base_price_is)
|
||||
|
||||
def is_available(self, now_dt: datetime=None) -> bool:
|
||||
"""
|
||||
@@ -271,7 +318,7 @@ class Item(LoggedModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None):
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None):
|
||||
"""
|
||||
This method is used to determine whether this Item is currently available
|
||||
for sale.
|
||||
@@ -285,12 +332,18 @@ class Item(LoggedModel):
|
||||
:raises ValueError: if you call this on an item which has variations associated with it.
|
||||
Please use the method on the ItemVariation object you are interested in.
|
||||
"""
|
||||
check_quotas = set(self.quotas.all())
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.select_related('subevent').filter(subevent=subevent)
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if not subevent and self.event.has_subevents:
|
||||
raise TypeError('You need to supply a subevent.')
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
if not check_quotas:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize
|
||||
if self.variations.count() > 0: # NOQA
|
||||
if self.has_variations: # NOQA
|
||||
raise ValueError('Do not call this directly on items which have variations '
|
||||
'but call this on their ItemVariation objects')
|
||||
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
|
||||
@@ -356,10 +409,11 @@ class ItemVariation(models.Model):
|
||||
def price(self):
|
||||
return self.default_price if self.default_price is not None else self.item.default_price
|
||||
|
||||
@property
|
||||
def net_price(self):
|
||||
tax_value = round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
|
||||
return self.price - tax_value
|
||||
def tax(self, price=None):
|
||||
price = price or self.price
|
||||
if not self.item.tax_rule:
|
||||
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
return self.item.tax_rule.tax(price)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
@@ -371,7 +425,7 @@ class ItemVariation(models.Model):
|
||||
if self.item:
|
||||
self.item.event.get_cache().clear()
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None) -> Tuple[int, int]:
|
||||
"""
|
||||
This method is used to determine whether this ItemVariation is currently
|
||||
available for sale in terms of quotas.
|
||||
@@ -383,9 +437,15 @@ class ItemVariation(models.Model):
|
||||
:param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
|
||||
:returns: any of the return codes of :py:meth:`Quota.availability()`.
|
||||
"""
|
||||
check_quotas = set(self.quotas.all())
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.filter(subevent=subevent).select_related('subevent')
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
if not subevent and self.item.event.has_subevents: # NOQA
|
||||
raise TypeError('You need to supply a subevent.')
|
||||
if not check_quotas:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize
|
||||
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
|
||||
@@ -402,6 +462,17 @@ class ItemAddOn(models.Model):
|
||||
An instance of this model indicates that buying a ticket of the time ``base_item``
|
||||
allows you to add up to ``max_count`` items from the category ``addon_category``
|
||||
to your order that will be associated with the base item.
|
||||
|
||||
:param base_item: The base item the add-ons are attached to
|
||||
:type base_item: Item
|
||||
:param addon_category: The category the add-on can be chosen from
|
||||
:type addon_category: ItemCategory
|
||||
:param min_count: The minimal number of add-ons to be chosen
|
||||
:type min_count: int
|
||||
:param max_count: The maximal number of add-ons to be chosen
|
||||
:type max_count: int
|
||||
:param position: An integer used for sorting
|
||||
:type position: int
|
||||
"""
|
||||
base_item = models.ForeignKey(
|
||||
Item,
|
||||
@@ -420,6 +491,12 @@ class ItemAddOn(models.Model):
|
||||
default=1,
|
||||
verbose_name=_('Maximum number')
|
||||
)
|
||||
price_included = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Add-Ons are included in the price'),
|
||||
help_text=_('If selected, adding add-ons to this ticket is free, even if the add-ons would normally cost '
|
||||
'money individually.')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
@@ -446,6 +523,7 @@ class Question(LoggedModel):
|
||||
* a multi-line string (``TYPE_TEXT``)
|
||||
* a boolean (``TYPE_BOOLEAN``)
|
||||
* a multiple choice option (``TYPE_CHOICE`` and ``TYPE_CHOICE_MULTIPLE``)
|
||||
* a file upload (``TYPE_FILE``))
|
||||
|
||||
:param event: The event this question belongs to
|
||||
:type event: Event
|
||||
@@ -463,13 +541,15 @@ class Question(LoggedModel):
|
||||
TYPE_BOOLEAN = "B"
|
||||
TYPE_CHOICE = "C"
|
||||
TYPE_CHOICE_MULTIPLE = "M"
|
||||
TYPE_FILE = "F"
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_NUMBER, _("Number")),
|
||||
(TYPE_STRING, _("Text (one line)")),
|
||||
(TYPE_TEXT, _("Multiline text")),
|
||||
(TYPE_BOOLEAN, _("Yes/No")),
|
||||
(TYPE_CHOICE, _("Choose one from a list")),
|
||||
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list"))
|
||||
(TYPE_CHOICE_MULTIPLE, _("Choose multiple from a list")),
|
||||
(TYPE_FILE, _("File upload")),
|
||||
)
|
||||
|
||||
event = models.ForeignKey(
|
||||
@@ -479,6 +559,11 @@ class Question(LoggedModel):
|
||||
question = I18nTextField(
|
||||
verbose_name=_("Question")
|
||||
)
|
||||
help_text = I18nTextField(
|
||||
verbose_name=_("Help text"),
|
||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=5,
|
||||
choices=TYPE_CHOICES,
|
||||
@@ -571,6 +656,8 @@ class Quota(LoggedModel):
|
||||
|
||||
:param event: The event this belongs to
|
||||
:type event: Event
|
||||
:param subevent: The event series date this belongs to, if event series are enabled
|
||||
:type subevent: SubEvent
|
||||
:param name: This quota's name
|
||||
:type name: str
|
||||
:param size: The number of items in this quota
|
||||
@@ -590,6 +677,13 @@ class Quota(LoggedModel):
|
||||
related_name="quotas",
|
||||
verbose_name=_("Event"),
|
||||
)
|
||||
subevent = models.ForeignKey(
|
||||
SubEvent,
|
||||
null=True, blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="quotas",
|
||||
verbose_name=pgettext_lazy('subevent', "Date"),
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Name")
|
||||
@@ -611,6 +705,9 @@ class Quota(LoggedModel):
|
||||
blank=True,
|
||||
verbose_name=_("Variations")
|
||||
)
|
||||
cached_availability_state = models.PositiveIntegerField(null=True, blank=True)
|
||||
cached_availability_number = models.PositiveIntegerField(null=True, blank=True)
|
||||
cached_availability_time = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Quota")
|
||||
@@ -625,11 +722,24 @@ class Quota(LoggedModel):
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
clear_cache = kwargs.pop('clear_cache', True)
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
if self.event and clear_cache:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def availability(self, now_dt: datetime=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
|
||||
def rebuild_cache(self, now_dt=None):
|
||||
self.cached_availability_time = None
|
||||
self.cached_availability_number = None
|
||||
self.cached_availability_state = None
|
||||
self.availability(now_dt=now_dt)
|
||||
|
||||
def cache_is_hot(self, now_dt=None):
|
||||
now_dt = now_dt or now()
|
||||
return self.cached_availability_time and (now_dt - self.cached_availability_time).total_seconds() < 120
|
||||
|
||||
def availability(
|
||||
self, now_dt: datetime=None, count_waitinglist=True, _cache=None, allow_cache=False
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
This method is used to determine whether Items or ItemVariations belonging
|
||||
to this quota should currently be available for sale.
|
||||
@@ -637,12 +747,26 @@ class Quota(LoggedModel):
|
||||
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
|
||||
and the second is the number of available tickets.
|
||||
"""
|
||||
if allow_cache and self.cache_is_hot() and count_waitinglist:
|
||||
return self.cached_availability_state, self.cached_availability_number
|
||||
|
||||
if _cache and count_waitinglist is not _cache.get('_count_waitinglist', True):
|
||||
_cache.clear()
|
||||
|
||||
if _cache is not None and self.pk in _cache:
|
||||
return _cache[self.pk]
|
||||
now_dt = now_dt or now()
|
||||
res = self._availability(now_dt, count_waitinglist)
|
||||
|
||||
if count_waitinglist and not self.cache_is_hot(now_dt):
|
||||
self.cached_availability_state = res[0]
|
||||
self.cached_availability_number = res[1]
|
||||
self.cached_availability_time = now_dt
|
||||
self.save(
|
||||
update_fields=['cached_availability_state', 'cached_availability_number', 'cached_availability_time'],
|
||||
clear_cache=False
|
||||
)
|
||||
|
||||
if _cache is not None:
|
||||
_cache[self.pk] = res
|
||||
_cache['_count_waitinglist'] = count_waitinglist
|
||||
@@ -684,11 +808,11 @@ class Quota(LoggedModel):
|
||||
now_dt = now_dt or now()
|
||||
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
|
||||
func = 'MAX'
|
||||
else:
|
||||
else: # NOQA
|
||||
func = 'GREATEST'
|
||||
|
||||
return Voucher.objects.filter(
|
||||
Q(event=self.event) &
|
||||
Q(event=self.event) & Q(subevent=self.subevent) &
|
||||
Q(block_quota=True) &
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
|
||||
Q(Q(self._position_lookup) | Q(quota=self))
|
||||
@@ -699,7 +823,7 @@ class Quota(LoggedModel):
|
||||
def count_waiting_list_pending(self) -> int:
|
||||
from pretix.base.models import WaitingListEntry
|
||||
return WaitingListEntry.objects.filter(
|
||||
Q(voucher__isnull=True) &
|
||||
Q(voucher__isnull=True) & Q(subevent=self.subevent) &
|
||||
self._position_lookup
|
||||
).distinct().count()
|
||||
|
||||
@@ -708,7 +832,7 @@ class Quota(LoggedModel):
|
||||
|
||||
now_dt = now_dt or now()
|
||||
return CartPosition.objects.filter(
|
||||
Q(event=self.event) &
|
||||
Q(event=self.event) & Q(subevent=self.subevent) &
|
||||
Q(expires__gte=now_dt) &
|
||||
~Q(
|
||||
Q(voucher__isnull=False) & Q(voucher__block_quota=True)
|
||||
@@ -722,14 +846,14 @@ class Quota(LoggedModel):
|
||||
|
||||
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event
|
||||
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
|
||||
).values('id').distinct().count()
|
||||
|
||||
def count_paid_orders(self):
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event
|
||||
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
|
||||
).values('id').distinct().count()
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -5,7 +5,10 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.signals import logentry_object_link
|
||||
|
||||
|
||||
class LogEntry(models.Model):
|
||||
@@ -49,7 +52,7 @@ class LogEntry(models.Model):
|
||||
|
||||
@cached_property
|
||||
def display_object(self):
|
||||
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event
|
||||
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
|
||||
|
||||
if self.content_type.model_class() is Event:
|
||||
return ''
|
||||
@@ -66,7 +69,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'code': co.code
|
||||
}),
|
||||
'val': co.code,
|
||||
'val': escape(co.code),
|
||||
}
|
||||
elif isinstance(co, Voucher):
|
||||
a_text = _('Voucher {val}…')
|
||||
@@ -76,7 +79,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'voucher': co.id
|
||||
}),
|
||||
'val': co.code[:6],
|
||||
'val': escape(co.code[:6]),
|
||||
}
|
||||
elif isinstance(co, Item):
|
||||
a_text = _('Product {val}')
|
||||
@@ -86,7 +89,17 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'item': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, SubEvent):
|
||||
a_text = pgettext_lazy('subevent', 'Date {val}')
|
||||
a_map = {
|
||||
'href': reverse('control:event.subevent', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
'subevent': co.id
|
||||
}),
|
||||
'val': escape(str(co))
|
||||
}
|
||||
elif isinstance(co, Quota):
|
||||
a_text = _('Quota {val}')
|
||||
@@ -96,7 +109,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'quota': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, ItemCategory):
|
||||
a_text = _('Category {val}')
|
||||
@@ -106,7 +119,7 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'category': co.id
|
||||
}),
|
||||
'val': co.name,
|
||||
'val': escape(co.name),
|
||||
}
|
||||
elif isinstance(co, Question):
|
||||
a_text = _('Question {val}')
|
||||
@@ -116,7 +129,17 @@ class LogEntry(models.Model):
|
||||
'organizer': self.event.organizer.slug,
|
||||
'question': co.id
|
||||
}),
|
||||
'val': co.question,
|
||||
'val': escape(co.question),
|
||||
}
|
||||
elif isinstance(co, TaxRule):
|
||||
a_text = _('Tax rule {val}')
|
||||
a_map = {
|
||||
'href': reverse('control:event.settings.tax.edit', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
'rule': co.id
|
||||
}),
|
||||
'val': escape(co.name),
|
||||
}
|
||||
|
||||
if a_text and a_map:
|
||||
@@ -125,6 +148,9 @@ class LogEntry(models.Model):
|
||||
elif a_text:
|
||||
return a_text
|
||||
else:
|
||||
for receiver, response in logentry_object_link.send(self.event, logentry=self):
|
||||
if response:
|
||||
return response
|
||||
return ''
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -2,23 +2,31 @@ import copy
|
||||
import json
|
||||
import os
|
||||
import string
|
||||
from datetime import datetime
|
||||
from datetime import datetime, time
|
||||
from decimal import Decimal
|
||||
from typing import List, Union
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import pytz
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import F, Sum
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.encoding import escape_uri_path
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_countries.fields import CountryField
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
|
||||
from ..decimal import round_decimal
|
||||
from .base import LoggedModel
|
||||
from .event import Event
|
||||
from .event import Event, SubEvent
|
||||
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
||||
|
||||
|
||||
@@ -70,18 +78,14 @@ class Order(LoggedModel):
|
||||
:type payment_date: datetime
|
||||
:param payment_provider: The payment provider selected by the user
|
||||
:type payment_provider: str
|
||||
:param payment_fee: The payment fee calculated at checkout time
|
||||
:type payment_fee: decimal.Decimal
|
||||
:param payment_fee_tax_value: The absolute amount of tax included in the payment fee
|
||||
:type payment_fee_tax_value: decimal.Decimal
|
||||
:param payment_fee_tax_rate: The tax rate applied to the payment fee (in percent)
|
||||
:type payment_fee_tax_rate: decimal.Decimal
|
||||
:param payment_info: Arbitrary information stored by the payment provider
|
||||
:type payment_info: str
|
||||
:param total: The total amount of the order, including the payment fee
|
||||
:type total: decimal.Decimal
|
||||
:param comment: An internal comment that will only be visible to staff, and never displayed to the user
|
||||
:type comment: str
|
||||
:param download_reminder_sent: A field to indicate whether a download reminder has been sent.
|
||||
:type download_reminder_sent: boolean
|
||||
:param meta_info: Additional meta information on the order, JSON-encoded.
|
||||
:type meta_info: str
|
||||
"""
|
||||
@@ -139,18 +143,6 @@ class Order(LoggedModel):
|
||||
max_length=255,
|
||||
verbose_name=_("Payment provider")
|
||||
)
|
||||
payment_fee = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
default=0, verbose_name=_("Payment method fee")
|
||||
)
|
||||
payment_fee_tax_rate = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Payment method fee tax rate")
|
||||
)
|
||||
payment_fee_tax_value = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
default=0, verbose_name=_("Payment method fee tax")
|
||||
)
|
||||
payment_info = models.TextField(
|
||||
verbose_name=_("Payment information"),
|
||||
null=True, blank=True
|
||||
@@ -171,6 +163,10 @@ class Order(LoggedModel):
|
||||
expiry_reminder_sent = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
|
||||
download_reminder_sent = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
meta_info = models.TextField(
|
||||
verbose_name=_("Meta information"),
|
||||
null=True, blank=True
|
||||
@@ -205,29 +201,11 @@ class Order(LoggedModel):
|
||||
self.assign_code()
|
||||
if not self.datetime:
|
||||
self.datetime = now()
|
||||
if self.payment_fee_tax_rate is None:
|
||||
self._calculate_tax()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _calculate_tax(self):
|
||||
"""
|
||||
Calculates the taxes on the payment fees and sets the parameters payment_fee_tax_rate
|
||||
and payment_fee_tax_value accordingly.
|
||||
"""
|
||||
self.payment_fee_tax_rate = self.event.settings.get('tax_rate_default')
|
||||
if self.payment_fee_tax_rate:
|
||||
self.payment_fee_tax_value = round_decimal(
|
||||
self.payment_fee * (1 - 100 / (100 + self.payment_fee_tax_rate)))
|
||||
else:
|
||||
self.payment_fee_tax_value = Decimal('0.00')
|
||||
|
||||
@property
|
||||
def payment_fee_net(self):
|
||||
return self.payment_fee - self.payment_fee_tax_value
|
||||
|
||||
@cached_property
|
||||
def tax_total(self):
|
||||
return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + self.payment_fee_tax_value
|
||||
return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + (self.fees.aggregate(s=Sum('tax_value'))['s'] or 0)
|
||||
|
||||
@property
|
||||
def net_total(self):
|
||||
@@ -264,7 +242,16 @@ class Order(LoggedModel):
|
||||
"""
|
||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=datetime)
|
||||
|
||||
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
if self.event.has_subevents and modify_deadline:
|
||||
modify_deadline = min([
|
||||
modify_deadline.datetime(se)
|
||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||
])
|
||||
elif modify_deadline:
|
||||
modify_deadline = modify_deadline.datetime(self.event)
|
||||
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
return False
|
||||
if self.event.settings.get('invoice_address_asked', as_type=bool):
|
||||
@@ -278,6 +265,9 @@ class Order(LoggedModel):
|
||||
|
||||
@property
|
||||
def can_user_cancel(self) -> bool:
|
||||
"""
|
||||
Returns whether or not this order can be canceled by the user.
|
||||
"""
|
||||
positions = self.positions.all().select_related('item')
|
||||
cancelable = all([op.item.allow_cancel for op in positions])
|
||||
return self.event.settings.cancel_allow_user and cancelable
|
||||
@@ -289,6 +279,41 @@ class Order(LoggedModel):
|
||||
and not self.event.settings.get('payment_term_expire_automatically')
|
||||
)
|
||||
|
||||
@property
|
||||
def ticket_download_date(self):
|
||||
"""
|
||||
Returns the first date the tickets for this order can be downloaded or ``None`` if there is no
|
||||
restriction.
|
||||
"""
|
||||
dl_date = self.event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
|
||||
if dl_date:
|
||||
if self.event.has_subevents:
|
||||
dl_date = min([
|
||||
dl_date.datetime(se)
|
||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||
])
|
||||
else:
|
||||
dl_date = dl_date.datetime(self.event)
|
||||
return dl_date
|
||||
|
||||
@property
|
||||
def payment_term_last(self):
|
||||
tz = pytz.timezone(self.event.settings.timezone)
|
||||
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
|
||||
if term_last:
|
||||
if self.event.has_subevents:
|
||||
term_last = min([
|
||||
term_last.datetime(se).date()
|
||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||
])
|
||||
else:
|
||||
term_last = term_last.datetime(self.event).date()
|
||||
term_last = make_aware(datetime.combine(
|
||||
term_last,
|
||||
time(hour=23, minute=59, second=59)
|
||||
), tz)
|
||||
return term_last
|
||||
|
||||
def _can_be_paid(self) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||
@@ -296,9 +321,9 @@ class Order(LoggedModel):
|
||||
'late': _("The payment can not be accepted as it the order is expired and you configured that no late "
|
||||
"payments should be accepted in the payment settings."),
|
||||
}
|
||||
|
||||
if self.event.settings.get('payment_term_last'):
|
||||
if now() > self.event.payment_term_last:
|
||||
term_last = self.payment_term_last
|
||||
if term_last:
|
||||
if now() > term_last:
|
||||
return error_messages['late_lastdate']
|
||||
|
||||
if self.status == self.STATUS_PENDING:
|
||||
@@ -308,7 +333,7 @@ class Order(LoggedModel):
|
||||
|
||||
return self._is_still_available()
|
||||
|
||||
def _is_still_available(self, now_dt: datetime=None) -> Union[bool, str]:
|
||||
def _is_still_available(self, now_dt: datetime=None, count_waitinglist=True) -> Union[bool, str]:
|
||||
error_messages = {
|
||||
'unavailable': _('The ordered product "{item}" is no longer available.'),
|
||||
}
|
||||
@@ -317,14 +342,16 @@ class Order(LoggedModel):
|
||||
quota_cache = {}
|
||||
try:
|
||||
for i, op in enumerate(positions):
|
||||
quotas = list(op.item.quotas.all()) if op.variation is None else list(op.variation.quotas.all())
|
||||
quotas = list(op.quotas)
|
||||
if len(quotas) == 0:
|
||||
raise Quota.QuotaExceededException(error_messages['unavailable'])
|
||||
raise Quota.QuotaExceededException(error_messages['unavailable'].format(
|
||||
item=str(op.item) + (' - ' + str(op.variation) if op.variation else '')
|
||||
))
|
||||
|
||||
for quota in quotas:
|
||||
if quota.id not in quota_cache:
|
||||
quota_cache[quota.id] = quota
|
||||
quota.cached_availability = quota.availability(now_dt)[1]
|
||||
quota.cached_availability = quota.availability(now_dt, count_waitinglist=count_waitinglist)[1]
|
||||
else:
|
||||
# Use cached version
|
||||
quota = quota_cache[quota.id]
|
||||
@@ -339,6 +366,59 @@ class Order(LoggedModel):
|
||||
return str(e)
|
||||
return True
|
||||
|
||||
def send_mail(self, subject: str, template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any]=None, log_entry_type: str='pretix.event.order.email.sent',
|
||||
user: User=None, headers: dict=None, sender: str=None):
|
||||
"""
|
||||
Sends an email to the user that placed this order. Basically, this method does two things:
|
||||
|
||||
* Call ``pretix.base.services.mail.mail`` with useful values for the ``event``, ``locale``, ``recipient`` and
|
||||
``order`` parameters.
|
||||
|
||||
* Create a ``LogEntry`` with the email contents.
|
||||
|
||||
:param subject: Subject of the email
|
||||
:param template: LazyI18nString or template filename, see ``pretix.base.services.mail.mail`` for more details
|
||||
:param context: Dictionary to use for rendering the template
|
||||
:param log_entry_type: Key to be used for the log entry
|
||||
:param user: Administrative user who triggered this mail to be sent
|
||||
:param headers: Dictionary with additional mail headers
|
||||
:param sender: Custom email sender.
|
||||
"""
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
|
||||
recipient = self.email
|
||||
email_content = render_mail(template, context)[0]
|
||||
try:
|
||||
with language(self.locale):
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers, sender
|
||||
)
|
||||
except SendMailException:
|
||||
raise
|
||||
else:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def answerfile_name(instance, filename: str) -> str:
|
||||
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
||||
event = (instance.cartposition if instance.cartposition else instance.orderposition.order).event
|
||||
return 'cachedfiles/answers/{org}/{ev}/{secret}.{filename}'.format(
|
||||
org=event.organizer.slug,
|
||||
ev=event.slug,
|
||||
secret=secret,
|
||||
filename=escape_uri_path(filename),
|
||||
)
|
||||
|
||||
|
||||
class QuestionAnswer(models.Model):
|
||||
"""
|
||||
@@ -370,12 +450,52 @@ class QuestionAnswer(models.Model):
|
||||
QuestionOption, related_name='answers', blank=True
|
||||
)
|
||||
answer = models.TextField()
|
||||
file = models.FileField(
|
||||
null=True, blank=True, upload_to=answerfile_name
|
||||
)
|
||||
|
||||
@property
|
||||
def backend_file_url(self):
|
||||
if self.file:
|
||||
if self.orderposition:
|
||||
return reverse('control:event.order.download.answer', kwargs={
|
||||
'code': self.orderposition.order.code,
|
||||
'event': self.orderposition.order.event.slug,
|
||||
'organizer': self.orderposition.order.event.organizer.slug,
|
||||
'answer': self.pk,
|
||||
})
|
||||
return ""
|
||||
|
||||
@property
|
||||
def frontend_file_url(self):
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
|
||||
if self.file:
|
||||
if self.orderposition:
|
||||
url = eventreverse(self.orderposition.order.event, 'presale:event.order.download.answer', kwargs={
|
||||
'order': self.orderposition.order.code,
|
||||
'secret': self.orderposition.order.secret,
|
||||
'answer': self.pk,
|
||||
})
|
||||
else:
|
||||
url = eventreverse(self.cartposition.event, 'presale:event.cart.download.answer', kwargs={
|
||||
'answer': self.pk,
|
||||
})
|
||||
|
||||
return url
|
||||
return ""
|
||||
|
||||
@property
|
||||
def file_name(self):
|
||||
return self.file.name.split('.', 1)[-1]
|
||||
|
||||
def __str__(self):
|
||||
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
|
||||
return str(_("Yes"))
|
||||
elif self.question.type == Question.TYPE_BOOLEAN and self.answer == "False":
|
||||
return str(_("No"))
|
||||
elif self.question.type == Question.TYPE_FILE:
|
||||
return str(_("<file>"))
|
||||
else:
|
||||
return self.answer
|
||||
|
||||
@@ -389,6 +509,8 @@ class AbstractPosition(models.Model):
|
||||
"""
|
||||
A position can either be one line of an order or an item placed in a cart.
|
||||
|
||||
:param subevent: The date in the event series, if event series are enabled
|
||||
:type subevent: SubEvent
|
||||
:param item: The selected item
|
||||
:type item: Item
|
||||
:param variation: The selected ItemVariation or null, if the item has no variations
|
||||
@@ -405,7 +527,15 @@ class AbstractPosition(models.Model):
|
||||
:type attendee_email: str
|
||||
:param voucher: A voucher that has been applied to this sale
|
||||
:type voucher: Voucher
|
||||
:param meta_info: Additional meta information on the position, JSON-encoded.
|
||||
:type meta_info: str
|
||||
"""
|
||||
subevent = models.ForeignKey(
|
||||
SubEvent,
|
||||
null=True, blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=pgettext_lazy("subevent", "Date"),
|
||||
)
|
||||
item = models.ForeignKey(
|
||||
Item,
|
||||
verbose_name=_("Item"),
|
||||
@@ -438,10 +568,21 @@ class AbstractPosition(models.Model):
|
||||
addon_to = models.ForeignKey(
|
||||
'self', null=True, blank=True, on_delete=models.CASCADE, related_name='addons'
|
||||
)
|
||||
meta_info = models.TextField(
|
||||
verbose_name=_("Meta information"),
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def meta_info_data(self):
|
||||
if self.meta_info:
|
||||
return json.loads(self.meta_info)
|
||||
else:
|
||||
return {}
|
||||
|
||||
def cache_answers(self):
|
||||
"""
|
||||
Creates two properties on the object.
|
||||
@@ -466,6 +607,97 @@ class AbstractPosition(models.Model):
|
||||
def net_price(self):
|
||||
return self.price - self.tax_value
|
||||
|
||||
@property
|
||||
def quotas(self):
|
||||
return (self.item.quotas.filter(subevent=self.subevent)
|
||||
if self.variation is None
|
||||
else self.variation.quotas.filter(subevent=self.subevent))
|
||||
|
||||
|
||||
class OrderFee(models.Model):
|
||||
"""
|
||||
An OrderFee objet represents a fee that is added to the order total independently of
|
||||
the actual positions. This might for example be a payment or a shipping fee.
|
||||
"""
|
||||
FEE_TYPE_PAYMENT = "payment"
|
||||
FEE_TYPE_SHIPPING = "shipping"
|
||||
FEE_TYPE_OTHER = "other"
|
||||
FEE_TYPES = (
|
||||
(FEE_TYPE_PAYMENT, _("Payment fee")),
|
||||
(FEE_TYPE_SHIPPING, _("Shipping fee")),
|
||||
(FEE_TYPE_OTHER, _("Other fees")),
|
||||
)
|
||||
|
||||
value = models.DecimalField(
|
||||
decimal_places=2, max_digits=10,
|
||||
verbose_name=_("Value")
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
verbose_name=_("Order"),
|
||||
related_name='fees',
|
||||
on_delete=models.PROTECT
|
||||
)
|
||||
fee_type = models.CharField(
|
||||
max_length=100, choices=FEE_TYPES
|
||||
)
|
||||
description = models.CharField(max_length=190, blank=True)
|
||||
internal_type = models.CharField(max_length=255, blank=True)
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
tax_rule = models.ForeignKey(
|
||||
'TaxRule',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
|
||||
@property
|
||||
def net_value(self):
|
||||
return self.value - self.tax_value
|
||||
|
||||
def __str__(self):
|
||||
if self.description:
|
||||
return '{} - {}'.format(self.get_fee_type_display(), self.description)
|
||||
else:
|
||||
return self.get_fee_type_display()
|
||||
|
||||
def __repr__(self):
|
||||
return '<OrderFee: type %s, value %d>' % (
|
||||
self.fee_type, self.value
|
||||
)
|
||||
|
||||
def _calculate_tax(self):
|
||||
try:
|
||||
ia = self.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
|
||||
if not self.tax_rule and self.fee_type == "payment" and self.order.event.settings.tax_rate_default:
|
||||
self.tax_rule = self.order.event.settings.tax_rate_default
|
||||
|
||||
if self.tax_rule:
|
||||
if self.tax_rule.tax_applicable(ia):
|
||||
tax = self.tax_rule.tax(self.value, base_price_is='gross')
|
||||
self.tax_rate = tax.rate
|
||||
self.tax_value = tax.tax
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_rate = Decimal('0.00')
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_rate = Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.tax_rate is None:
|
||||
self._calculate_tax()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class OrderPosition(AbstractPosition):
|
||||
"""
|
||||
@@ -487,6 +719,15 @@ class OrderPosition(AbstractPosition):
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
tax_rule = models.ForeignKey(
|
||||
'TaxRule',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
@@ -546,11 +787,22 @@ class OrderPosition(AbstractPosition):
|
||||
)
|
||||
|
||||
def _calculate_tax(self):
|
||||
self.tax_rate = self.item.tax_rate
|
||||
if self.tax_rate:
|
||||
self.tax_value = round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
|
||||
self.tax_rule = self.item.tax_rule
|
||||
try:
|
||||
ia = self.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
if self.tax_rule:
|
||||
if self.tax_rule.tax_applicable(ia):
|
||||
tax = self.tax_rule.tax(self.price, base_price_is='gross')
|
||||
self.tax_rate = tax.rate
|
||||
self.tax_value = tax.tax
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_rate = Decimal('0.00')
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_rate = Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.tax_rate is None:
|
||||
@@ -591,6 +843,9 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_("Expiration date"),
|
||||
db_index=True
|
||||
)
|
||||
includes_tax = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Cart position")
|
||||
@@ -603,25 +858,33 @@ class CartPosition(AbstractPosition):
|
||||
|
||||
@property
|
||||
def tax_rate(self):
|
||||
return self.item.tax_rate
|
||||
if self.includes_tax:
|
||||
return self.item.tax(self.price, base_price_is='gross').rate
|
||||
else:
|
||||
return Decimal('0.00')
|
||||
|
||||
@property
|
||||
def tax_value(self):
|
||||
if not self.tax_rate:
|
||||
if self.includes_tax:
|
||||
return self.item.tax(self.price, base_price_is='gross').tax
|
||||
else:
|
||||
return Decimal('0.00')
|
||||
return round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
|
||||
|
||||
|
||||
class InvoiceAddress(models.Model):
|
||||
last_modified = models.DateTimeField(auto_now=True)
|
||||
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address')
|
||||
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
||||
name = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=False)
|
||||
zipcode = models.CharField(max_length=30, verbose_name=_('ZIP code'), blank=False)
|
||||
city = models.CharField(max_length=255, verbose_name=_('City'), blank=False)
|
||||
country = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
||||
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'))
|
||||
country_old = models.CharField(max_length=255, verbose_name=_('Country'), blank=False)
|
||||
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
|
||||
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
||||
help_text=_('Only for business customers within the EU.'))
|
||||
vat_id_validated = models.BooleanField(default=False)
|
||||
|
||||
|
||||
def cachedticket_name(instance, filename: str) -> str:
|
||||
@@ -671,3 +934,17 @@ def cachedticket_delete(sender, instance, **kwargs):
|
||||
if instance.file:
|
||||
# Pass false so FileField doesn't save the model.
|
||||
instance.file.delete(False)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=CachedCombinedTicket)
|
||||
def cachedcombinedticket_delete(sender, instance, **kwargs):
|
||||
if instance.file:
|
||||
# Pass false so FileField doesn't save the model.
|
||||
instance.file.delete(False)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=QuestionAnswer)
|
||||
def answer_delete(sender, instance, **kwargs):
|
||||
if instance.file:
|
||||
# Pass false so FileField doesn't save the model.
|
||||
instance.file.delete(False)
|
||||
|
||||