forked from CGM_Public/pretix_original
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ffe55453 | ||
|
|
ab865e716f | ||
|
|
0bf1832b23 | ||
|
|
650adb9235 | ||
|
|
e2d55fed0d | ||
|
|
aef751dbee | ||
|
|
cd084fe8d1 | ||
|
|
c68b6116a2 | ||
|
|
f0db879c9c | ||
|
|
07d8a3d765 | ||
|
|
e35e264d81 | ||
|
|
d537e6a869 | ||
|
|
d4dd1861a9 | ||
|
|
3019a31fbb | ||
|
|
303b9912ff | ||
|
|
0259b2e5b9 | ||
|
|
5c7e8029f4 | ||
|
|
08e3fd3141 | ||
|
|
30123fd6ff | ||
|
|
3955299983 | ||
|
|
b5d0df3ca7 | ||
|
|
22c65da9d1 | ||
|
|
578c1ecfaf | ||
|
|
d8d00a7e26 | ||
|
|
37f0f7a138 | ||
|
|
f61e9367ec | ||
|
|
3c3e59e932 | ||
|
|
29e22a0c6c | ||
|
|
0d1f424425 | ||
|
|
1c01e23867 | ||
|
|
f763a8694b | ||
|
|
675b853b29 | ||
|
|
2434bf14d5 | ||
|
|
70fbbfe2a0 | ||
|
|
e096898a05 | ||
|
|
3fbccf3f64 | ||
|
|
36585395f1 | ||
|
|
e4b0a1613f | ||
|
|
1192e474c5 | ||
|
|
e48ea99e48 | ||
|
|
072f2a0ee9 | ||
|
|
aecb536a34 | ||
|
|
a68686cb06 | ||
|
|
ba8cf3e01e | ||
|
|
b0c5189c4b | ||
|
|
d44eb67dec | ||
|
|
58d36b08e2 | ||
|
|
98906731e3 | ||
|
|
035a4b0928 | ||
|
|
85fbe666ea | ||
|
|
741d0bc686 | ||
|
|
ded539ce7a | ||
|
|
c53fd25d1c | ||
|
|
da32621c55 | ||
|
|
4ccf33af03 | ||
|
|
a5af7a70f3 | ||
|
|
16ab0d29d6 | ||
|
|
05ad9022c0 | ||
|
|
fef211b220 | ||
|
|
6aee1ee41f | ||
|
|
bab7f9b1f3 | ||
|
|
340e7afd06 | ||
|
|
cb83c9cff2 | ||
|
|
911a8fed06 | ||
|
|
eb8b43fe36 | ||
|
|
2a15dc57d8 | ||
|
|
67678e35bb | ||
|
|
2f00db8081 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -8,6 +8,7 @@ src/static/fileupload/* linguist-vendored
|
||||
src/static/vuejs/* linguist-vendored
|
||||
src/static/select2/* linguist-vendored
|
||||
src/static/charts/* linguist-vendored
|
||||
src/static/iframeresizer/* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored
|
||||
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@ pypi:
|
||||
- pip install -U pip wheel setuptools
|
||||
- XDG_CACHE_HOME=/cache pip3 install -Ur src/requirements.txt -r src/requirements/dev.txt -r src/requirements/py34.txt
|
||||
- cd src
|
||||
- python setup.py sdist
|
||||
- pip install dist/pretix-*.tar.gz
|
||||
- python -m pretix migrate
|
||||
- python -m pretix check
|
||||
- python setup.py sdist upload
|
||||
- python setup.py bdist_wheel upload
|
||||
tags:
|
||||
|
||||
@@ -12,7 +12,7 @@ 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. ``PREFIX_CONFIG_FILE`` environment variable
|
||||
1. ``PRETIX_CONFIG_FILE`` environment variable
|
||||
2. ``/etc/pretix/pretix.cfg``
|
||||
3. ``~/.pretix.cfg``
|
||||
4. ``pretix.cfg`` in the current working directory
|
||||
@@ -288,4 +288,4 @@ various places like order codes, secrets in the ticket QR codes, etc. Example::
|
||||
voucher_code=16
|
||||
|
||||
.. _Python documentation: https://docs.python.org/3/library/configparser.html?highlight=configparser#supported-ini-file-structure
|
||||
.. _Celery documentation: http://docs.celeryproject.org/en/latest/configuration.html
|
||||
.. _Celery documentation: http://docs.celeryproject.org/en/latest/userguide/configuration.html
|
||||
|
||||
@@ -21,11 +21,12 @@ Field Type Description
|
||||
===================================== ========================== =======================================================
|
||||
id integer Internal ID of the check-in list
|
||||
name string The internal name of the check-in list
|
||||
all_products boolean If ``True``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case.
|
||||
all_products boolean If ``true``, the check-in lists contains tickets of all products in this event. The ``limit_products`` field is ignored in this case.
|
||||
limit_products list of integers List of item IDs to include in this list.
|
||||
subevent integer ID of the date inside an event series this list belongs to (or ``null``).
|
||||
position_count integer Number of tickets that match this list (read-only).
|
||||
checkin_count integer Number of check-ins performed on this list (read-only).
|
||||
include_pending boolean If ``true``, the check-in list also contains tickets from orders in pending state.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 1.10
|
||||
@@ -36,6 +37,10 @@ checkin_count integer Number of check
|
||||
|
||||
The ``positions`` endpoints have been added.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The ``include_pending`` field has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -71,6 +76,7 @@ Endpoints
|
||||
"position_count": 456,
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
]
|
||||
@@ -111,6 +117,7 @@ Endpoints
|
||||
"position_count": 456,
|
||||
"all_products": true,
|
||||
"limit_products": [],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -156,6 +163,7 @@ Endpoints
|
||||
"position_count": 0,
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
@@ -204,6 +212,7 @@ Endpoints
|
||||
"position_count": 42,
|
||||
"all_products": false,
|
||||
"limit_products": [1, 2],
|
||||
"include_pending": false,
|
||||
"subevent": null
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ payment_fee_tax_value money (string) Tax value inclu
|
||||
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
|
||||
checkin_attention boolean If ``True``, the check-in app should show a warning
|
||||
that this ticket requires special attention if a ticket
|
||||
of this order is scanned.
|
||||
invoice_address object Invoice address information (can be ``null``)
|
||||
├ last_modified datetime Last modification date of the address
|
||||
├ company string Customer company name
|
||||
@@ -88,6 +91,10 @@ downloads list of objects List of ticket
|
||||
First write operations (``…/mark_paid/``, ``…/mark_pending/``, ``…/mark_canceled/``, ``…/mark_expired/``) have been added.
|
||||
The attribute ``invoice_address.internal_reference`` has been added.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The field ``checkin_attention`` has been added.
|
||||
|
||||
.. _order-position-resource:
|
||||
|
||||
Order position resource
|
||||
@@ -175,6 +182,7 @@ Order endpoints
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"checkin_attention": false,
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"is_business": True,
|
||||
@@ -282,6 +290,7 @@ Order endpoints
|
||||
"fees": [],
|
||||
"total": "23.00",
|
||||
"comment": "",
|
||||
"checkin_attention": false,
|
||||
"invoice_address": {
|
||||
"last_modified": "2017-12-01T10:00:00Z",
|
||||
"company": "Sample company",
|
||||
|
||||
@@ -4,7 +4,8 @@ Tax rules
|
||||
Resource description
|
||||
--------------------
|
||||
|
||||
Tax rules specify how tax should be calculated for specific products.
|
||||
Tax rules specify how tax should be calculated for specific products. Custom taxation rule sets are currently to
|
||||
available via the API.
|
||||
|
||||
.. rst-class:: rest-resource-table
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ The provider class
|
||||
|
||||
.. automethod:: order_control_refund_perform
|
||||
|
||||
.. automethod:: is_implicit
|
||||
|
||||
|
||||
Additional views
|
||||
----------------
|
||||
|
||||
@@ -4,56 +4,7 @@
|
||||
List of plugins
|
||||
===============
|
||||
|
||||
The following plugins are shipped with pretix and are supported in the same
|
||||
ways that pretix itself is:
|
||||
A detailed list of plugins that are available for pretix can be found on the
|
||||
`project website`_.
|
||||
|
||||
* Bank transfer
|
||||
* PayPal
|
||||
* Stripe
|
||||
* Check-in lists
|
||||
* pretixdroid
|
||||
* Report exporter
|
||||
* Send out emails
|
||||
* Statistics
|
||||
* PDF ticket output
|
||||
|
||||
The following plugins are not shipped with pretix but are maintained by the
|
||||
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
|
||||
pretix installation:
|
||||
|
||||
* Campaign tracking
|
||||
* Integration with Google Analytics and Facebook Pixel
|
||||
* Integration with Slack
|
||||
* Integration with MailChimp
|
||||
|
||||
The following plugins are from independent third-party authors, so we can make
|
||||
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
|
||||
.. _Cartshare: https://github.com/pretix/pretix-cartshare
|
||||
.. _Pages: https://github.com/pretix/pretix-pages
|
||||
.. _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
|
||||
.. _project website: https://pretix.eu/about/en/plugins
|
||||
|
||||
@@ -15,6 +15,10 @@ uses to communicate with the pretix server.
|
||||
negotiated live, so clients which do not need this feature can ignore the change. For this reason, the API version
|
||||
has not been increased and is still set to 3.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
Support for checking in unpaid tickets has been added.
|
||||
|
||||
|
||||
.. http:post:: /pretixdroid/api/(organizer)/(event)/redeem/
|
||||
|
||||
@@ -49,6 +53,9 @@ uses to communicate with the pretix server.
|
||||
check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection
|
||||
failure.
|
||||
|
||||
You **may** set the additional parameter ``ignore_unpaid`` to indicate that the check-in should be performed even
|
||||
if the order is in pending state.
|
||||
|
||||
If questions are supported and required, you will receive a dictionary ``questions`` containing details on the
|
||||
particular questions to ask. To answer them, just re-send your redemption request with additional parameters of
|
||||
the form ``answer_<question>=<answer>``, e.g. ``answer_12=24``.
|
||||
@@ -73,6 +80,7 @@ uses to communicate with the pretix server.
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
@@ -97,6 +105,7 @@ uses to communicate with the pretix server.
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
},
|
||||
"questions": [
|
||||
@@ -142,6 +151,7 @@ uses to communicate with the pretix server.
|
||||
"attendee_name": "Peter Higgs",
|
||||
"attention": false,
|
||||
"redeemed": true,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
}
|
||||
}
|
||||
@@ -201,6 +211,7 @@ uses to communicate with the pretix server.
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
@@ -244,6 +255,7 @@ uses to communicate with the pretix server.
|
||||
"attendee_name": "Peter Higgs",
|
||||
"redeemed": false,
|
||||
"attention": false,
|
||||
"checkin_allowed": true,
|
||||
"paid": true
|
||||
},
|
||||
...
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-r ../src/requirements.txt
|
||||
sphinx
|
||||
sphinx==1.6.*
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib-httpdomain
|
||||
sphinxcontrib-images
|
||||
|
||||
@@ -100,6 +100,16 @@ taxes" at the end of the page.
|
||||
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.
|
||||
|
||||
Custom tax rules
|
||||
----------------
|
||||
|
||||
If you have very special requirements for the conditions in which VAT will or will not be charged, you can use the
|
||||
"Custom tax rules" section instead of the options listed above. Here, you can create a set of rules consisting of
|
||||
conditions (i.e. a country or a type of customer) and actions (i.e. do or do not charge VAT).
|
||||
|
||||
The rules will then be checked from top to bottom and the first matching rule will be used to decide if VAT will be
|
||||
charged to the user.
|
||||
|
||||
Taxation of payment fees
|
||||
------------------------
|
||||
|
||||
|
||||
@@ -101,4 +101,43 @@ voucher's settings.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
pretix Button
|
||||
-------------
|
||||
|
||||
Instead of a product list, you can also display just a single button. When pressed, the button will add a number of
|
||||
products associated with the button to the cart and will immediately proceed to checkout if the operation succeeded.
|
||||
You can try out this behavior here:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<pretix-button event="https://pretix.eu/demo/democon/" items="item_6424=1">Buy ticket!</pretix-button>
|
||||
<noscript>
|
||||
<div class="pretix-widget">
|
||||
<div class="pretix-widget-info-message">
|
||||
JavaScript is disabled in your browser. To access our ticket shop without javascript, please <a target="_blank" href="https://pretix.eu/demo/democon/">click here</a>.
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<br><br>
|
||||
|
||||
You can embed the pretix Button just like the pretix Widget. Just like above, first embed the CSS and JavaScript
|
||||
resources. Then, instead of the ``pretix-widget`` tag, use the ``pretix-button`` tag::
|
||||
|
||||
<pretix-button event="https://pretix.eu/demo/democon/" items="item_6424=1">
|
||||
Buy ticket!
|
||||
</pretix-button>
|
||||
|
||||
As you can see, the ``pretix-button`` element takes an additional ``items`` attribute that specifies the items that
|
||||
should be added to the cart. The syntax of this attribute is ``item_ITEMID=1,item_ITEMID=2,variation_ITEMID_VARID=4``
|
||||
where ``ITEMID`` are the internal IDs of items to be added and ``VARID`` are the internal IDs of variations of those
|
||||
items, if the items have variations.
|
||||
|
||||
Just as the widget, the button supports the optional attributes ``voucher`` and ``skip-ssl-check``.
|
||||
|
||||
You can style the button using the ``pretix-button`` CSS class.
|
||||
|
||||
.. versionchanged:: 1.13
|
||||
|
||||
The pretix Button has been added in version 1.13.
|
||||
|
||||
.. _Let's Encrypt: https://letsencrypt.org/
|
||||
|
||||
@@ -12,6 +12,12 @@ If you look into pretix' settings, you are required to fill in two keys:
|
||||
|
||||
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.
|
||||
|
||||
.. warning::
|
||||
|
||||
Unfortunately, PayPal tries to confuse you by having multiple APIs with different keys. You really need to
|
||||
go to https://developer.paypal.com for the API we use, not to your normal account settings!
|
||||
|
||||
Click on "Log In" in the top-right corner and log in with your PayPal account.
|
||||
|
||||
.. image:: img/paypal2.png
|
||||
@@ -46,8 +52,8 @@ 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.
|
||||
Then, enter the webhook URL that you find on the pretix settings page. If you use pretix Hosted, this is always ``https://pretix.eu/_paypal/webhook/``.
|
||||
Tick the box "All events" and save.
|
||||
|
||||
.. image:: img/paypal8.png
|
||||
:class: screenshot
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.12.0"
|
||||
__version__ = "1.13.1"
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
|
||||
|
||||
class EventPermission(BasePermission):
|
||||
@@ -24,16 +23,13 @@ class EventPermission(BasePermission):
|
||||
required_permission = None
|
||||
|
||||
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())
|
||||
try:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/control/middleware.py
|
||||
assert_session_valid(request)
|
||||
except SessionInvalid:
|
||||
return False
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
|
||||
else request.user)
|
||||
|
||||
@@ -12,7 +12,8 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count')
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending')
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
|
||||
@@ -135,7 +135,7 @@ class OrderSerializer(I18nAwareModelSerializer):
|
||||
model = Order
|
||||
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
|
||||
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
|
||||
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value')
|
||||
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value', 'checkin_attention')
|
||||
|
||||
|
||||
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -126,7 +126,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.checkinlist.include_pending else [Order.STATUS_PAID],
|
||||
subevent=self.checkinlist.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
def round_decimal(dec):
|
||||
|
||||
def round_decimal(dec, currency=None):
|
||||
if currency:
|
||||
places = settings.CURRENCY_PLACES.get(currency, 2)
|
||||
return Decimal(dec).quantize(
|
||||
Decimal('1') / 10 ** places, ROUND_HALF_UP
|
||||
)
|
||||
return Decimal(dec).quantize(Decimal('0.01'), ROUND_HALF_UP)
|
||||
|
||||
@@ -55,7 +55,7 @@ class AnswerFilesExporter(BaseExporter):
|
||||
i.file.close()
|
||||
|
||||
with open(os.path.join(d, 'tmp.zip'), 'rb') as zipf:
|
||||
return 'answers.zip', 'application/zip', zipf.read()
|
||||
return '{}_answers.zip'.format(self.event.slug), 'application/zip', zipf.read()
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_answers")
|
||||
|
||||
@@ -21,7 +21,7 @@ class MailExporter(BaseExporter):
|
||||
pos = OrderPosition.objects.filter(
|
||||
order__event=self.event, order__status__in=form_data['status']
|
||||
).values('attendee_email')
|
||||
data = "\r\n".join(set(a['email'] for a in addrs)
|
||||
data = "\r\n".join(set(a['email'] for a in addrs if a['email'])
|
||||
| set(a['attendee_email'] for a in pos if a['attendee_email']))
|
||||
return '{}_pretixemails.txt'.format(self.event.slug), 'text/plain', data.encode("utf-8")
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class OrderListExporter(BaseExporter):
|
||||
row.append(', '.join([i.number for i in order.invoices.all()]))
|
||||
writer.writerow(row)
|
||||
|
||||
return 'orders.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
return '{}_orders.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
|
||||
class QuotaListExporter(BaseExporter):
|
||||
|
||||
@@ -11,16 +11,16 @@ from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
|
||||
from .validators import PlaceholderValidator # NOQA
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseI18nModelForm(i18nfield.forms.BaseI18nModelForm):
|
||||
# compatibility shim for django-i18nfield library
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.pop('event', None)
|
||||
if event:
|
||||
kwargs['locales'] = event.settings.get('locales')
|
||||
self.event = kwargs.pop('event', None)
|
||||
if self.event:
|
||||
kwargs['locales'] = self.event.settings.get('locales')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ class I18nFormSet(i18nfield.forms.I18nModelFormSet):
|
||||
# compatibility shim for django-i18nfield library
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.pop('event', None)
|
||||
if event:
|
||||
kwargs['locales'] = event.settings.get('locales')
|
||||
self.event = kwargs.pop('event', None)
|
||||
if self.event:
|
||||
kwargs['locales'] = self.event.settings.get('locales')
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
@@ -232,5 +232,13 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
||||
'your country is currently not available. We will therefore '
|
||||
'need to charge VAT on your invoice. You can get the tax amount '
|
||||
'back via the VAT reimbursement process.'))
|
||||
except vat_moss.errors.WebServiceError:
|
||||
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
|
||||
self.instance.vat_id_validated = False
|
||||
if self.request and self.vat_warning:
|
||||
messages.warning(self.request, _('Your VAT ID could not be checked, as the VAT checking service of '
|
||||
'your country returned an incorrect result. We will therefore '
|
||||
'need to charge VAT on your invoice. Please contact support to '
|
||||
'resolve this manually.'))
|
||||
else:
|
||||
self.instance.vat_id_validated = False
|
||||
|
||||
@@ -12,6 +12,8 @@ from i18nfield.forms import I18nFormField # noqa
|
||||
from i18nfield.strings import LazyI18nString # noqa
|
||||
from i18nfield.utils import I18nJSONEncoder # noqa
|
||||
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
class LazyDate:
|
||||
def __init__(self, value):
|
||||
@@ -24,6 +26,18 @@ class LazyDate:
|
||||
return date_format(self.value, "SHORT_DATE_FORMAT")
|
||||
|
||||
|
||||
class LazyCurrencyNumber:
|
||||
def __init__(self, value, currency):
|
||||
self.value = value
|
||||
self.currency = currency
|
||||
|
||||
def __format__(self, format_spec):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return money_filter(self.value, self.currency)
|
||||
|
||||
|
||||
class LazyNumber:
|
||||
def __init__(self, value, decimal_pos=2):
|
||||
self.value = value
|
||||
|
||||
@@ -24,6 +24,7 @@ from reportlab.platypus import (
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import Event, Invoice
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
class BaseInvoiceRenderer:
|
||||
@@ -376,14 +377,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
"1",
|
||||
localize(line.tax_rate) + " %",
|
||||
localize(line.net_value) + " " + self.invoice.event.currency,
|
||||
localize(line.gross_value) + " " + self.invoice.event.currency,
|
||||
money_filter(line.net_value, self.invoice.event.currency),
|
||||
money_filter(line.gross_value, self.invoice.event.currency),
|
||||
))
|
||||
else:
|
||||
tdata.append((
|
||||
Paragraph(line.description, self.stylesheet['Normal']),
|
||||
"1",
|
||||
localize(line.gross_value) + " " + self.invoice.event.currency,
|
||||
money_filter(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
|
||||
@@ -391,12 +392,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if has_taxes:
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', '', '', localize(total) + " " + self.invoice.event.currency
|
||||
pgettext('invoice', 'Invoice total'), '', '', '', money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
|
||||
else:
|
||||
tdata.append([
|
||||
pgettext('invoice', 'Invoice total'), '', localize(total) + " " + self.invoice.event.currency
|
||||
pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
colwidths = [a * doc.width for a in (.65, .05, .30)]
|
||||
|
||||
@@ -436,9 +437,9 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
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,
|
||||
money_filter(gross - tax, self.invoice.event.currency),
|
||||
money_filter(gross, self.invoice.event.currency),
|
||||
money_filter(tax, self.invoice.event.currency),
|
||||
''
|
||||
])
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# 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:"],
|
||||
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
|
||||
'report-uri': ["/csp_report/"],
|
||||
}
|
||||
if 'Content-Security-Policy' in resp:
|
||||
|
||||
46
src/pretix/base/migrations/0081_auto_20180220_1031.py
Normal file
46
src/pretix/base/migrations/0081_auto_20180220_1031.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-20 10:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0080_question_ask_during_checkin'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='include_pending',
|
||||
field=models.BooleanField(default=False, verbose_name='Include pending orders'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='event',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date. If you do not set this value, the presale will end after the end date of your event.', null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='logentry',
|
||||
name='event',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pretixbase.Event'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='ask_during_checkin',
|
||||
field=models.BooleanField(default=False, help_text='This will only work if you handle your check-in with pretixdroid 1.8 or newer or pretixdesk 0.2 or newer.', verbose_name='Ask during check-in instead of in the ticket buying process'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='subevent',
|
||||
name='presale_end',
|
||||
field=models.DateTimeField(blank=True, help_text='Optional. No products will be sold after this date. If you do not set this value, the presale will end after the end date of your event.', null=True, verbose_name='End of presale'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='require_2fa',
|
||||
field=models.BooleanField(default=False, verbose_name='Two-factor authentification is required to log in'),
|
||||
),
|
||||
]
|
||||
25
src/pretix/base/migrations/0082_auto_20180222_0938.py
Normal file
25
src/pretix/base/migrations/0082_auto_20180222_0938.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-22 09:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0081_auto_20180220_1031'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='checkin_attention',
|
||||
field=models.BooleanField(default=False, help_text='If you set this, the check-in app will show a visible warning that tickets of this order require special attention. This will not show any details or custom message, so you need to brief your check-in staff how to handle these cases.', verbose_name='Requires special attention'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='checkinlist',
|
||||
name='include_pending',
|
||||
field=models.BooleanField(default=False, help_text='With this option, people will be able to check in even if the order have not been paid. This only works with pretixdesk 0.3.0 or newer or pretixdroid 1.9 or newer.', verbose_name='Include pending orders'),
|
||||
),
|
||||
]
|
||||
25
src/pretix/base/migrations/0083_auto_20180228_2102.py
Normal file
25
src/pretix/base/migrations/0083_auto_20180228_2102.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.10 on 2018-02-28 21:02
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0082_auto_20180222_0938'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='taxrule',
|
||||
name='custom_rules',
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='orderfee',
|
||||
name='fee_type',
|
||||
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('service', 'Service fee'), ('other', 'Other fees')], max_length=100),
|
||||
),
|
||||
]
|
||||
41
src/pretix/base/migrations/0084_questionoption_position.py
Normal file
41
src/pretix/base/migrations/0084_questionoption_position.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-03-03 16:41
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def set_position(apps, schema_editor):
|
||||
Question = apps.get_model('pretixbase', 'Question')
|
||||
for q in Question.objects.all():
|
||||
for i, option in enumerate(q.options.all()):
|
||||
option.position = i
|
||||
option.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0083_auto_20180228_2102'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='questionoption',
|
||||
options={'ordering': ('position', 'id'), 'verbose_name': 'Question option', 'verbose_name_plural': 'Question options'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='questionoption',
|
||||
name='position',
|
||||
field=models.IntegerField(default=0),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='question',
|
||||
name='position',
|
||||
field=models.PositiveIntegerField(default=0, verbose_name='Position'),
|
||||
),
|
||||
migrations.RunPython(
|
||||
set_position,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
@@ -36,7 +36,7 @@ def cached_file_delete(sender, instance, **kwargs):
|
||||
|
||||
class LoggingMixin:
|
||||
|
||||
def log_action(self, action, data=None, user=None, api_token=None):
|
||||
def log_action(self, action, data=None, user=None, api_token=None, save=True):
|
||||
"""
|
||||
Create a LogEntry object that is related to this object.
|
||||
See the LogEntry documentation for details.
|
||||
@@ -60,10 +60,12 @@ class LoggingMixin:
|
||||
logentry = LogEntry(content_object=self, user=user, action_type=action, event=event, api_token=api_token)
|
||||
if data:
|
||||
logentry.data = json.dumps(data, cls=CustomJSONEncoder)
|
||||
logentry.save()
|
||||
if save:
|
||||
logentry.save()
|
||||
|
||||
if action in get_all_notification_types():
|
||||
notify.apply_async(args=(logentry.pk,))
|
||||
if action in get_all_notification_types():
|
||||
notify.apply_async(args=(logentry.pk,))
|
||||
return logentry
|
||||
|
||||
|
||||
class LoggedModel(models.Model, LoggingMixin):
|
||||
|
||||
@@ -14,6 +14,11 @@ class CheckinList(LoggedModel):
|
||||
limit_products = models.ManyToManyField('Item', verbose_name=_("Limit to products"), blank=True)
|
||||
subevent = models.ForeignKey('SubEvent', null=True, blank=True,
|
||||
verbose_name=pgettext_lazy('subevent', 'Date'))
|
||||
include_pending = models.BooleanField(verbose_name=pgettext_lazy('checkin', 'Include pending orders'),
|
||||
default=False,
|
||||
help_text=_('With this option, people will be able to check in even if the '
|
||||
'order have not been paid. This only works with pretixdesk '
|
||||
'0.3.0 or newer or pretixdroid 1.9 or newer.'))
|
||||
|
||||
@staticmethod
|
||||
def annotate_with_numbers(qs, event):
|
||||
@@ -29,7 +34,7 @@ class CheckinList(LoggedModel):
|
||||
# position and to the list in question. Then, we check that it also belongs to the
|
||||
# correct subevent (just to be sure) and aggregate over lists (so, over everything,
|
||||
# since we filtered by lists).
|
||||
cqs = Checkin.objects.filter(
|
||||
cqs_paid = Checkin.objects.filter(
|
||||
position__order__event=event,
|
||||
position__order__status=Order.STATUS_PAID,
|
||||
list=OuterRef('pk')
|
||||
@@ -41,12 +46,24 @@ class CheckinList(LoggedModel):
|
||||
).order_by().values('list').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
cqs_paid_and_pending = Checkin.objects.filter(
|
||||
position__order__event=event,
|
||||
position__order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
|
||||
list=OuterRef('pk')
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(position__subevent=OuterRef('subevent'))
|
||||
| (Q(position__subevent__isnull=True))
|
||||
).order_by().values('list').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now for the hard part: getting all order positions that contribute to this list. This
|
||||
# requires us to use TWO subqueries. The first one, pqs_all, will only be used for check-in
|
||||
# lists that contain all the products of the event. This is the simpler one, it basically
|
||||
# looks like the check-in counter above.
|
||||
pqs_all = OrderPosition.objects.filter(
|
||||
pqs_all_paid = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
).filter(
|
||||
@@ -57,13 +74,24 @@ class CheckinList(LoggedModel):
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
pqs_all_paid_and_pending = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING]
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Now we need a subquery for the case of checkin lists that are limited to certain
|
||||
# products. We cannot use OuterRef("limit_products") since that would do a cross-product
|
||||
# with the products table and we'd get duplicate rows in the output with different annotations
|
||||
# on them, which isn't useful at all. Therefore, we need to add a second layer of subqueries
|
||||
# to retrieve all of those items and then check if the item_id is IN this subquery result.
|
||||
pqs_limited = OrderPosition.objects.filter(
|
||||
pqs_limited_paid = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
|
||||
@@ -75,17 +103,44 @@ class CheckinList(LoggedModel):
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
pqs_limited_paid_and_pending = OrderPosition.objects.filter(
|
||||
order__event=event,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING],
|
||||
item_id__in=Subquery(Item.objects.filter(checkinlist__pk=OuterRef(OuterRef('pk'))).values('pk'))
|
||||
).filter(
|
||||
# This assumes that in an event with subevents, *all* positions have subevents
|
||||
# and *all* checkin lists have a subevent assigned
|
||||
Q(subevent=OuterRef('subevent'))
|
||||
| (Q(subevent__isnull=True))
|
||||
).order_by().values('order__event').annotate(
|
||||
c=Count('*')
|
||||
).values('c')
|
||||
|
||||
# Finally, we put all of this together. We force empty subquery aggregates to 0 by using Coalesce()
|
||||
# and decide which subquery to use for this row. In the end, we compute an integer percentage in case
|
||||
# we want to display a progress bar.
|
||||
return qs.annotate(
|
||||
checkin_count=Coalesce(Subquery(cqs, output_field=models.IntegerField()), 0),
|
||||
position_count=Coalesce(Case(
|
||||
When(all_products=True, then=Subquery(pqs_all, output_field=models.IntegerField())),
|
||||
default=Subquery(pqs_limited, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
), 0)
|
||||
checkin_count=Coalesce(
|
||||
Case(
|
||||
When(include_pending=True, then=Subquery(cqs_paid_and_pending, output_field=models.IntegerField())),
|
||||
default=Subquery(cqs_paid, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
),
|
||||
0
|
||||
),
|
||||
position_count=Coalesce(
|
||||
Case(
|
||||
When(all_products=True, include_pending=False,
|
||||
then=Subquery(pqs_all_paid, output_field=models.IntegerField())),
|
||||
When(all_products=True, include_pending=True,
|
||||
then=Subquery(pqs_all_paid_and_pending, output_field=models.IntegerField())),
|
||||
When(all_products=False, include_pending=False,
|
||||
then=Subquery(pqs_limited_paid, output_field=models.IntegerField())),
|
||||
default=Subquery(pqs_limited_paid_and_pending, output_field=models.IntegerField()),
|
||||
output_field=models.IntegerField()
|
||||
),
|
||||
0
|
||||
)
|
||||
).annotate(
|
||||
percent=Case(
|
||||
When(position_count__gt=0, then=F('checkin_count') * 100 / F('position_count')),
|
||||
|
||||
@@ -43,7 +43,7 @@ class EventMixin:
|
||||
Returns a shorter formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"SHORT_DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
@@ -55,7 +55,7 @@ class EventMixin:
|
||||
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)
|
||||
tz = tz or self.timezone
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return ""
|
||||
return _date(
|
||||
@@ -68,7 +68,7 @@ class EventMixin:
|
||||
Returns a formatted string containing the start date of the event with respect
|
||||
to the current locale and to the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
return _date(
|
||||
self.date_from.astimezone(tz),
|
||||
"DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT"
|
||||
@@ -79,7 +79,7 @@ class EventMixin:
|
||||
Returns a formatted string containing the start time of the event, ignoring
|
||||
the ``show_times`` setting.
|
||||
"""
|
||||
tz = tz or pytz.timezone(self.settings.timezone)
|
||||
tz = tz or self.timezone
|
||||
return _date(
|
||||
self.date_from.astimezone(tz), "TIME_FORMAT"
|
||||
)
|
||||
@@ -90,7 +90,7 @@ class EventMixin:
|
||||
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)
|
||||
tz = tz or self.timezone
|
||||
if not self.settings.show_date_to or not self.date_to:
|
||||
return ""
|
||||
return _date(
|
||||
@@ -100,23 +100,30 @@ class EventMixin:
|
||||
|
||||
def get_date_range_display(self, tz=None) -> str:
|
||||
"""
|
||||
Returns a formatted string containing the start date and the event date
|
||||
Returns a formatted string containing the start date and the end 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)
|
||||
tz = tz or self.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 timezone(self):
|
||||
return pytz.timezone(self.settings.timezone)
|
||||
|
||||
@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
|
||||
if self.presale_end:
|
||||
return now() > self.presale_end
|
||||
elif self.date_to:
|
||||
return now() > self.date_to
|
||||
else:
|
||||
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
||||
|
||||
@property
|
||||
def presale_is_running(self):
|
||||
@@ -126,9 +133,7 @@ class EventMixin:
|
||||
"""
|
||||
if self.presale_start and now() < self.presale_start:
|
||||
return False
|
||||
if self.presale_end and now() > self.presale_end:
|
||||
return False
|
||||
return True
|
||||
return not self.presale_has_ended
|
||||
|
||||
@property
|
||||
def event_microdata(self):
|
||||
@@ -229,7 +234,8 @@ class Event(EventMixin, LoggedModel):
|
||||
presale_end = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("End of presale"),
|
||||
help_text=_("Optional. No products will be sold after this date."),
|
||||
help_text=_("Optional. No products will be sold after this date. If you do not set this value, the presale "
|
||||
"will end after the end date of your event."),
|
||||
)
|
||||
presale_start = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
@@ -262,6 +268,13 @@ class Event(EventMixin, LoggedModel):
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
@property
|
||||
def presale_has_ended(self):
|
||||
if self.has_subevents:
|
||||
return self.presale_end and now() > self.presale_end
|
||||
else:
|
||||
return super().presale_has_ended
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
obj = super().save(*args, **kwargs)
|
||||
self.cache.clear()
|
||||
@@ -323,10 +336,6 @@ class Event(EventMixin, LoggedModel):
|
||||
else:
|
||||
return get_connection(fail_silently=False)
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
return pytz.timezone(self.settings.timezone)
|
||||
|
||||
@property
|
||||
def payment_term_last(self):
|
||||
"""
|
||||
@@ -590,7 +599,8 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
presale_end = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
verbose_name=_("End of presale"),
|
||||
help_text=_("Optional. No products will be sold after this date."),
|
||||
help_text=_("Optional. No products will be sold after this date. If you do not set this value, the presale "
|
||||
"will end after the end date of your event."),
|
||||
)
|
||||
presale_start = models.DateTimeField(
|
||||
null=True, blank=True,
|
||||
@@ -646,6 +656,10 @@ class SubEvent(EventMixin, LoggedModel):
|
||||
data.update({v.property.name: v.value for v in self.meta_values.select_related('property').all()})
|
||||
return data
|
||||
|
||||
@property
|
||||
def currency(self):
|
||||
return self.event.currency
|
||||
|
||||
def allow_delete(self):
|
||||
return self.event.subevents.count() > 1
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ class Invoice(models.Model):
|
||||
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)
|
||||
internal_reference = models.TextField(blank=True)
|
||||
|
||||
|
||||
@@ -682,8 +682,9 @@ class Question(LoggedModel):
|
||||
blank=True,
|
||||
help_text=_('This question will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.IntegerField(
|
||||
default=0
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
ask_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||
@@ -779,10 +780,16 @@ class Question(LoggedModel):
|
||||
class QuestionOption(models.Model):
|
||||
question = models.ForeignKey('Question', related_name='options')
|
||||
answer = I18nCharField(verbose_name=_('Answer'))
|
||||
position = models.IntegerField(default=0)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.answer)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Question option")
|
||||
verbose_name_plural = _("Question options")
|
||||
ordering = ('position', 'id')
|
||||
|
||||
|
||||
class Quota(LoggedModel):
|
||||
"""
|
||||
@@ -799,7 +806,7 @@ class Quota(LoggedModel):
|
||||
|
||||
Please read the documentation section on quotas carefully before doing
|
||||
anything with quotas. This might confuse you otherwise.
|
||||
http://docs.pretix.eu/en/latest/development/concepts.html#restriction-by-number
|
||||
https://docs.pretix.eu/en/latest/development/concepts.html#quotas
|
||||
|
||||
The AVAILABILITY_* constants represent various states of a quota allowing
|
||||
its items/variations to be up for sale.
|
||||
|
||||
@@ -162,6 +162,13 @@ class Order(LoggedModel):
|
||||
help_text=_("The text entered in this field will not be visible to the user and is available for your "
|
||||
"convenience.")
|
||||
)
|
||||
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 tickets of this order require '
|
||||
'special attention. This will not show any details or custom message, so you need to brief your '
|
||||
'check-in staff how to handle these cases.')
|
||||
)
|
||||
expiry_reminder_sent = models.BooleanField(
|
||||
default=False
|
||||
)
|
||||
@@ -395,6 +402,9 @@ class Order(LoggedModel):
|
||||
"""
|
||||
from pretix.base.services.mail import SendMailException, mail, render_mail
|
||||
|
||||
if not self.email:
|
||||
return
|
||||
|
||||
with language(self.locale):
|
||||
recipient = self.email
|
||||
try:
|
||||
@@ -658,10 +668,12 @@ class OrderFee(models.Model):
|
||||
"""
|
||||
FEE_TYPE_PAYMENT = "payment"
|
||||
FEE_TYPE_SHIPPING = "shipping"
|
||||
FEE_TYPE_SERVICE = "service"
|
||||
FEE_TYPE_OTHER = "other"
|
||||
FEE_TYPES = (
|
||||
(FEE_TYPE_PAYMENT, _("Payment fee")),
|
||||
(FEE_TYPE_SHIPPING, _("Shipping fee")),
|
||||
(FEE_TYPE_SERVICE, _("Service fee")),
|
||||
(FEE_TYPE_OTHER, _("Other fees")),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
@@ -8,6 +9,7 @@ from i18nfield.fields import I18nCharField
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
|
||||
class TaxedPrice:
|
||||
@@ -23,6 +25,13 @@ class TaxedPrice:
|
||||
def __repr__(self):
|
||||
return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross))
|
||||
|
||||
def print(self, currency):
|
||||
return '{} + {}% = {}'.format(
|
||||
money_filter(self.net, currency),
|
||||
localize(self.rate),
|
||||
money_filter(self.gross, currency)
|
||||
)
|
||||
|
||||
|
||||
TAXED_ZERO = TaxedPrice(
|
||||
gross=Decimal('0.00'),
|
||||
@@ -80,6 +89,7 @@ class TaxRule(LoggedModel):
|
||||
help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, '
|
||||
'if configured above.'),
|
||||
)
|
||||
custom_rules = models.TextField(blank=True, null=True)
|
||||
|
||||
def allow_delete(self):
|
||||
from pretix.base.models.orders import OrderFee, OrderPosition
|
||||
@@ -129,10 +139,12 @@ class TaxRule(LoggedModel):
|
||||
|
||||
if base_price_is == 'gross':
|
||||
gross = base_price
|
||||
net = gross - round_decimal(base_price * (1 - 100 / (100 + self.rate)))
|
||||
net = round_decimal(gross - (base_price * (1 - 100 / (100 + self.rate))),
|
||||
self.event.currency if self.event else None)
|
||||
elif base_price_is == 'net':
|
||||
net = base_price
|
||||
gross = round_decimal(net * (1 + self.rate / 100))
|
||||
gross = round_decimal((net * (1 + self.rate / 100)),
|
||||
self.event.currency if self.event else None)
|
||||
else:
|
||||
raise ValueError('Unknown base price type: {}'.format(base_price_is))
|
||||
|
||||
@@ -141,7 +153,27 @@ class TaxRule(LoggedModel):
|
||||
rate=self.rate, name=self.name
|
||||
)
|
||||
|
||||
def get_matching_rule(self, invoice_address):
|
||||
rules = json.loads(self.custom_rules)
|
||||
for r in rules:
|
||||
if r['country'] == 'EU' and str(invoice_address.country) not in EU_COUNTRIES:
|
||||
continue
|
||||
if r['country'] not in ('ZZ', 'EU') and r['country'] != str(invoice_address.country):
|
||||
continue
|
||||
if r['address_type'] == 'individual' and invoice_address.is_business:
|
||||
continue
|
||||
if r['address_type'] in ('business', 'business_vat_id') and not invoice_address.is_business:
|
||||
continue
|
||||
if r['address_type'] == 'business_vat_id' and (not invoice_address.vat_id or not invoice_address.vat_id_validated):
|
||||
continue
|
||||
return r
|
||||
return {'action': 'vat'}
|
||||
|
||||
def is_reverse_charge(self, invoice_address):
|
||||
if self.custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
return rule['action'] == 'reverse'
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
return False
|
||||
|
||||
@@ -160,6 +192,10 @@ class TaxRule(LoggedModel):
|
||||
return False
|
||||
|
||||
def tax_applicable(self, invoice_address):
|
||||
if self.custom_rules:
|
||||
rule = self.get_matching_rule(invoice_address)
|
||||
return rule.get('action', 'vat') == 'vat'
|
||||
|
||||
if not self.eu_reverse_charge:
|
||||
# No reverse charge rules? Always apply VAT!
|
||||
return True
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from decimal import Decimal
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -42,7 +42,7 @@ class Voucher(LoggedModel):
|
||||
:param max_usages: The number of times this voucher can be redeemed
|
||||
:type max_usages: int
|
||||
:param redeemed: The number of times this voucher already has been redeemed
|
||||
:type redeemed: bool
|
||||
:type redeemed: int
|
||||
:param valid_until: The expiration date of this voucher (optional)
|
||||
:type valid_until: datetime
|
||||
:param block_quota: If set to true, this voucher will reserve quota for its holder
|
||||
@@ -368,9 +368,15 @@ class Voucher(LoggedModel):
|
||||
"""
|
||||
if self.value is not None:
|
||||
if self.price_mode == 'set':
|
||||
return self.value
|
||||
p = self.value
|
||||
elif self.price_mode == 'subtract':
|
||||
return max(original_price - self.value, Decimal('0.00'))
|
||||
p = max(original_price - self.value, Decimal('0.00'))
|
||||
elif self.price_mode == 'percent':
|
||||
return round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
|
||||
p = round_decimal(original_price * (Decimal('100.00') - self.value) / Decimal('100.00'))
|
||||
else:
|
||||
p = original_price
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
if places < 2:
|
||||
return p.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
|
||||
return p
|
||||
return original_price
|
||||
|
||||
@@ -2,11 +2,12 @@ import logging
|
||||
from collections import OrderedDict, namedtuple
|
||||
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event, LogEntry
|
||||
from pretix.base.signals import register_notification_types
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.urls import build_absolute_uri
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -174,7 +175,7 @@ class ParametrizedOrderNotificationType(NotificationType):
|
||||
url=order_url
|
||||
)
|
||||
n.add_attribute(_('Order code'), order.code)
|
||||
n.add_attribute(_('Order total'), '{} {}'.format(localize(order.total), logentry.event.currency))
|
||||
n.add_attribute(_('Order total'), money_filter(order.total, logentry.event.currency))
|
||||
n.add_attribute(_('Order date'), date_format(order.datetime, 'SHORT_DATETIME_FORMAT'))
|
||||
n.add_attribute(_('Order status'), order.get_status_display())
|
||||
n.add_attribute(_('Order positions'), str(order.positions.count()))
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import Any, Dict, Union
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.dispatch import receiver
|
||||
from django.forms import Form
|
||||
@@ -15,11 +16,11 @@ from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import CartPosition, Event, Order, Quota
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateWrapper
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.signals import register_payment_providers
|
||||
from pretix.helpers.money import DecimalTextInput
|
||||
from pretix.presale.views import get_cart_total
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
@@ -50,6 +51,16 @@ class BasePaymentProvider:
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
@property
|
||||
def is_implicit(self) -> bool:
|
||||
"""
|
||||
Returns whether or whether not this payment provider is an "implicit" payment provider that will
|
||||
*always* and unconditionally be used if is_allowed() returns True and does not require any input.
|
||||
This is intended to be used by the FreePaymentProvider, which skips the payment choice page.
|
||||
By default, this returns ``False``. Please do not set this if you don't know exactly what you are doing.
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_meta(self) -> bool:
|
||||
"""
|
||||
@@ -81,10 +92,15 @@ class BasePaymentProvider:
|
||||
fee_abs = self.settings.get('_fee_abs', as_type=Decimal, default=0)
|
||||
fee_percent = self.settings.get('_fee_percent', as_type=Decimal, default=0)
|
||||
fee_reverse_calc = self.settings.get('_fee_reverse_calc', as_type=bool, default=True)
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
if fee_reverse_calc:
|
||||
return round_decimal((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price)
|
||||
return ((price + fee_abs) * (1 / (1 - fee_percent / 100)) - price).quantize(
|
||||
Decimal('1') / 10 ** places, ROUND_HALF_UP
|
||||
)
|
||||
else:
|
||||
return round_decimal(price * fee_percent / 100) + fee_abs
|
||||
return (price * fee_percent / 100 + fee_abs).quantize(
|
||||
Decimal('1') / 10 ** places, ROUND_HALF_UP
|
||||
)
|
||||
|
||||
@property
|
||||
def verbose_name(self) -> str:
|
||||
@@ -146,6 +162,7 @@ class BasePaymentProvider:
|
||||
.. WARNING:: It is highly discouraged to alter the ``_enabled`` field of the default
|
||||
implementation.
|
||||
"""
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
return OrderedDict([
|
||||
('_enabled',
|
||||
forms.BooleanField(
|
||||
@@ -156,7 +173,10 @@ class BasePaymentProvider:
|
||||
forms.DecimalField(
|
||||
label=_('Additional fee'),
|
||||
help_text=_('Absolute value'),
|
||||
required=False
|
||||
localize=True,
|
||||
required=False,
|
||||
decimal_places=places,
|
||||
widget=DecimalTextInput(places=places)
|
||||
)),
|
||||
('_fee_percent',
|
||||
forms.DecimalField(
|
||||
@@ -164,7 +184,8 @@ class BasePaymentProvider:
|
||||
help_text=_('Percentage of the order total. Note that this percentage will currently only '
|
||||
'be calculated on the summed price of sold tickets, not on other fees like e.g. shipping '
|
||||
'fees, if there are any.'),
|
||||
required=False
|
||||
localize=True,
|
||||
required=False,
|
||||
)),
|
||||
('_availability_date',
|
||||
RelativeDateField(
|
||||
@@ -552,6 +573,10 @@ class PaymentException(Exception):
|
||||
|
||||
class FreeOrderProvider(BasePaymentProvider):
|
||||
|
||||
@property
|
||||
def is_implicit(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
return True
|
||||
|
||||
@@ -112,7 +112,7 @@ class CartManager:
|
||||
def _check_presale_dates(self):
|
||||
if self.event.presale_start and self.now_dt < self.event.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
if self.event.presale_end and self.now_dt > self.event.presale_end:
|
||||
if self.event.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
def _extend_expiry_of_valid_existing_positions(self):
|
||||
@@ -188,7 +188,7 @@ class CartManager:
|
||||
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
|
||||
if op.subevent and op.subevent.presale_end and self.now_dt > op.subevent.presale_end:
|
||||
if op.subevent and op.subevent.presale_has_ended:
|
||||
raise CartError(error_messages['ended'])
|
||||
|
||||
if isinstance(op, self.AddOperation):
|
||||
@@ -667,7 +667,8 @@ def get_fees(event, request, total, invoice_address, provider):
|
||||
tax_rule=payment_fee_tax_rule
|
||||
))
|
||||
|
||||
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address):
|
||||
for recv, resp in fee_calculation_for_cart.send(sender=event, request=request, invoice_address=invoice_address,
|
||||
total=total):
|
||||
fees += resp
|
||||
|
||||
return fees
|
||||
|
||||
@@ -39,6 +39,7 @@ def notify(logentry_id: int):
|
||||
notify_global = {
|
||||
(ns.user, ns.method): ns.enabled
|
||||
for ns in NotificationSetting.objects.filter(
|
||||
event__isnull=True,
|
||||
action_type=logentry.action_type,
|
||||
user__pk__in=users.values_list('pk', flat=True)
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.i18n import (
|
||||
LazyDate, LazyLocaleException, LazyNumber, language,
|
||||
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, Item, ItemVariation, Order, OrderPosition, Quota,
|
||||
@@ -321,7 +321,7 @@ class OrderError(LazyLocaleException):
|
||||
def _check_date(event: Event, now_dt: datetime):
|
||||
if event.presale_start and now_dt < event.presale_start:
|
||||
raise OrderError(error_messages['not_started'])
|
||||
if event.presale_end and now_dt > event.presale_end:
|
||||
if event.presale_has_ended:
|
||||
raise OrderError(error_messages['ended'])
|
||||
|
||||
|
||||
@@ -361,7 +361,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
||||
cp.delete()
|
||||
break
|
||||
|
||||
if cp.subevent and cp.subevent.presale_end and now_dt > cp.subevent.presale_end:
|
||||
if cp.subevent and cp.subevent.presale_has_ended:
|
||||
err = err or error_messages['some_subevent_ended']
|
||||
cp.delete()
|
||||
break
|
||||
@@ -439,8 +439,8 @@ def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvid
|
||||
fees.append(OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee,
|
||||
internal_type=payment_provider.identifier))
|
||||
|
||||
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address,
|
||||
meta_info=meta_info, posiitons=positions):
|
||||
for recv, resp in order_fee_calculation.send(sender=event, invoice_address=address, total=total,
|
||||
meta_info=meta_info, positions=positions):
|
||||
fees += resp
|
||||
return fees
|
||||
|
||||
@@ -504,6 +504,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
|
||||
for fee in fees:
|
||||
fee.order = order
|
||||
fee._calculate_tax()
|
||||
if fee.tax_rule and not fee.tax_rule.pk:
|
||||
fee.tax_rule = None # TODO: deprecate
|
||||
fee.save()
|
||||
|
||||
OrderPosition.transform_cart_positions(positions, order)
|
||||
@@ -521,6 +523,9 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
if not pprov:
|
||||
raise OrderError(error_messages['internal'])
|
||||
|
||||
if email == settings.PRETIX_EMAIL_NONE_VALUE:
|
||||
email = None
|
||||
|
||||
addr = None
|
||||
if address is not None:
|
||||
try:
|
||||
@@ -542,44 +547,49 @@ def _perform_order(event: str, payment_provider: str, position_ids: List[str],
|
||||
invoice = order.invoices.last() # Might be generated by plugin already
|
||||
if event.settings.get('invoice_generate') == 'True' and invoice_qualified(order):
|
||||
if not invoice:
|
||||
invoice = generate_invoice(order, trigger_pdf=not event.settings.invoice_email_attachment)
|
||||
invoice = generate_invoice(
|
||||
order,
|
||||
trigger_pdf=not event.settings.invoice_email_attachment or not order.email
|
||||
)
|
||||
# send_mail will trigger PDF generation later
|
||||
|
||||
if order.payment_provider == 'free':
|
||||
email_template = event.settings.mail_text_order_free
|
||||
log_entry = 'pretix.event.order.email.order_free'
|
||||
else:
|
||||
email_template = event.settings.mail_text_order_placed
|
||||
log_entry = 'pretix.event.order.email.order_placed'
|
||||
if order.email:
|
||||
if order.payment_provider == 'free':
|
||||
email_template = event.settings.mail_text_order_free
|
||||
log_entry = 'pretix.event.order.email.order_free'
|
||||
else:
|
||||
email_template = event.settings.mail_text_order_placed
|
||||
log_entry = 'pretix.event.order.email.order_placed'
|
||||
|
||||
try:
|
||||
invoice_name = order.invoice_address.name
|
||||
invoice_company = order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
email_context = {
|
||||
'total': LazyNumber(order.total),
|
||||
'currency': event.currency,
|
||||
'date': LazyDate(order.expires),
|
||||
'event': event.name,
|
||||
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'payment_info': str(pprov.order_pending_mail_render(order)),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
email_subject = _('Your order: %(code)s') % {'code': order.code}
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
log_entry,
|
||||
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else []
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order received email could not be sent')
|
||||
try:
|
||||
invoice_name = order.invoice_address.name
|
||||
invoice_company = order.invoice_address.company
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
invoice_name = ""
|
||||
invoice_company = ""
|
||||
email_context = {
|
||||
'total': LazyNumber(order.total),
|
||||
'currency': event.currency,
|
||||
'total_with_currency': LazyCurrencyNumber(order.total, event.currency),
|
||||
'date': LazyDate(order.expires),
|
||||
'event': event.name,
|
||||
'url': build_absolute_uri(event, 'presale:event.order', kwargs={
|
||||
'order': order.code,
|
||||
'secret': order.secret
|
||||
}),
|
||||
'payment_info': str(pprov.order_pending_mail_render(order)),
|
||||
'invoice_name': invoice_name,
|
||||
'invoice_company': invoice_company,
|
||||
}
|
||||
email_subject = _('Your order: %(code)s') % {'code': order.code}
|
||||
try:
|
||||
order.send_mail(
|
||||
email_subject, email_template, email_context,
|
||||
log_entry,
|
||||
invoices=[invoice] if invoice and event.settings.invoice_email_attachment else []
|
||||
)
|
||||
except SendMailException:
|
||||
logger.exception('Order received email could not be sent')
|
||||
|
||||
return order.id
|
||||
|
||||
@@ -805,7 +815,7 @@ class OrderChangeManager:
|
||||
if price is None:
|
||||
price = get_price(item, variation, subevent=subevent, invoice_address=self._invoice_address)
|
||||
else:
|
||||
if item.tax_rule.tax_applicable(self._invoice_address):
|
||||
if item.tax_rule and item.tax_rule.tax_applicable(self._invoice_address):
|
||||
price = item.tax(price, base_price_is='gross')
|
||||
else:
|
||||
price = TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models import (
|
||||
AbstractPosition, InvoiceAddress, Item, ItemAddOn, ItemVariation, Voucher,
|
||||
)
|
||||
@@ -59,4 +60,8 @@ def get_price(item: Item, variation: ItemVariation = None,
|
||||
price.gross = price.net
|
||||
price.name = ''
|
||||
|
||||
price.gross = round_decimal(price.gross, item.event.currency)
|
||||
price.net = round_decimal(price.net, item.event.currency)
|
||||
price.tax = price.gross - price.net
|
||||
|
||||
return price
|
||||
|
||||
@@ -35,6 +35,10 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
|
||||
if (wle.item, wle.variation) in gone:
|
||||
continue
|
||||
|
||||
ev = (wle.subevent or event)
|
||||
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
|
||||
continue
|
||||
|
||||
quotas = (wle.variation.quotas.filter(subevent=wle.subevent)
|
||||
if wle.variation
|
||||
else wle.item.quotas.filter(subevent=wle.subevent))
|
||||
@@ -64,7 +68,9 @@ def assign_automatically(event_id: int, user_id: int=None, subevent_id: int=None
|
||||
|
||||
@receiver(signal=periodic_task)
|
||||
def process_waitinglist(sender, **kwargs):
|
||||
qs = Event.objects.prefetch_related('_settings_objects', 'organizer___settings_objects').select_related('organizer')
|
||||
qs = Event.objects.filter(
|
||||
live=True
|
||||
).prefetch_related('_settings_objects', 'organizer___settings_objects').select_related('organizer')
|
||||
for e in qs:
|
||||
if e.settings.waiting_list_enabled and e.settings.waiting_list_auto:
|
||||
if e.settings.waiting_list_enabled and e.settings.waiting_list_auto and e.presale_is_running:
|
||||
assign_automatically.apply_async(args=(e.pk,))
|
||||
|
||||
@@ -276,7 +276,7 @@ Your {event} team"""))
|
||||
'default': LazyI18nString.from_gettext(ugettext_noop("""Hello,
|
||||
|
||||
we successfully received your order for {event} with a total value
|
||||
of {total} {currency}. Please complete your payment before {date}.
|
||||
of {total_with_currency}. Please complete your payment before {date}.
|
||||
|
||||
{payment_info}
|
||||
|
||||
|
||||
@@ -291,7 +291,7 @@ an OrderedDict of (setting name, form field).
|
||||
"""
|
||||
|
||||
order_fee_calculation = EventPluginSignal(
|
||||
providing_args=['request']
|
||||
providing_args=['positions', 'invoice_address', 'meta_info', 'total']
|
||||
)
|
||||
"""
|
||||
This signals allows you to add fees to an order while it is being created. You are expected to
|
||||
@@ -300,7 +300,9 @@ return a list of ``OrderFee`` objects that are not yet saved to the database
|
||||
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event. A ``positions``
|
||||
argument will contain the cart positions and ``invoice_address`` the invoice address (useful for
|
||||
tax calculation). The argument ``meta_info`` contains the order's meta dictionary.
|
||||
tax calculation). The argument ``meta_info`` contains the order's meta dictionary. The ``total``
|
||||
keyword argument will contain the total cart sum without any fees. You should not rely on this
|
||||
``total`` value for fee calculations as other fees might interfere.
|
||||
"""
|
||||
|
||||
order_fee_type_name = EventPluginSignal(
|
||||
|
||||
55
src/pretix/base/templatetags/money.py
Normal file
55
src/pretix/base/templatetags/money.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from babel.numbers import format_currency
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.utils import translation
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter("money")
|
||||
def money_filter(value: Decimal, arg='', hide_currency=False):
|
||||
if isinstance(value, float) or isinstance(value, int):
|
||||
value = Decimal(value)
|
||||
if not isinstance(value, Decimal):
|
||||
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
|
||||
if not arg:
|
||||
raise ValueError("No currency passed.")
|
||||
|
||||
places = settings.CURRENCY_PLACES.get(arg, 2)
|
||||
rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
|
||||
if places < 2 and rounded != value:
|
||||
places = 2
|
||||
if hide_currency:
|
||||
return floatformat(value, places)
|
||||
|
||||
try:
|
||||
if rounded != value:
|
||||
# We display decimal places even if we shouldn't for this currency if rounding
|
||||
# would make the numbers incorrect. If this branch executes, it's likely a bug in
|
||||
# pretix, but we won't show wrong numbers!
|
||||
return '{} {}'.format(
|
||||
arg,
|
||||
floatformat(value, 2)
|
||||
)
|
||||
return format_currency(value, arg, locale=translation.get_language())
|
||||
except:
|
||||
return '{} {}'.format(
|
||||
arg,
|
||||
floatformat(value, places)
|
||||
)
|
||||
|
||||
|
||||
@register.filter("money_numberfield")
|
||||
def money_numberfield_filter(value: Decimal, arg=''):
|
||||
if isinstance(value, float) or isinstance(value, int):
|
||||
value = Decimal(value)
|
||||
if not isinstance(value, Decimal):
|
||||
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
|
||||
if not arg:
|
||||
raise ValueError("No currency passed.")
|
||||
|
||||
places = settings.CURRENCY_PLACES.get(arg, 2)
|
||||
return str(value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP))
|
||||
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@@ -11,7 +12,7 @@ class BlacklistValidator:
|
||||
# Validation logic
|
||||
if value in self.blacklist:
|
||||
raise ValidationError(
|
||||
_('This slug has an invalid value: %(value)s.'),
|
||||
_('This field has an invalid value: %(value)s.'),
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
)
|
||||
@@ -56,3 +57,11 @@ class OrganizerSlugBlacklistValidator(BlacklistValidator):
|
||||
'csp_report',
|
||||
'widget',
|
||||
]
|
||||
|
||||
|
||||
@deconstructible
|
||||
class EmailBlacklistValidator(BlacklistValidator):
|
||||
|
||||
blacklist = [
|
||||
settings.PRETIX_EMAIL_NONE_VALUE,
|
||||
]
|
||||
|
||||
@@ -36,7 +36,8 @@ class CheckinListForm(forms.ModelForm):
|
||||
'name',
|
||||
'all_products',
|
||||
'limit_products',
|
||||
'subevent'
|
||||
'subevent',
|
||||
'include_pending'
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
|
||||
@@ -4,8 +4,11 @@ from django.contrib.auth.hashers import check_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db.models import Q
|
||||
from django.forms import formset_factory
|
||||
from django.utils.timezone import get_current_timezone_name
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from django_countries import Countries
|
||||
from django_countries.fields import LazyTypedChoiceField
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
from pytz import common_timezones, timezone
|
||||
|
||||
@@ -666,10 +669,10 @@ class MailSettingsForm(SettingsForm):
|
||||
label=_("Text"),
|
||||
required=False,
|
||||
widget=I18nTextarea,
|
||||
help_text=_("Available placeholders: {event}, {total}, {currency}, {date}, {payment_info}, {url}, "
|
||||
"{invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{total}', '{currency}', '{date}', '{payment_info}',
|
||||
'{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
help_text=_("Available placeholders: {event}, {total_with_currency}, {total}, {currency}, {date}, "
|
||||
"{payment_info}, {url}, {invoice_name}, {invoice_company}"),
|
||||
validators=[PlaceholderValidator(['{event}', '{total_with_currency}', '{total}', '{currency}', '{date}',
|
||||
'{payment_info}', '{url}', '{invoice_name}', '{invoice_company}'])]
|
||||
)
|
||||
mail_text_order_paid = I18nFormField(
|
||||
label=_("Text"),
|
||||
@@ -907,6 +910,43 @@ class CommentForm(I18nModelForm):
|
||||
}
|
||||
|
||||
|
||||
class CountriesAndEU(Countries):
|
||||
override = {
|
||||
'ZZ': _('Any country'),
|
||||
'EU': _('European Union')
|
||||
}
|
||||
first = ['ZZ', 'EU']
|
||||
|
||||
|
||||
class TaxRuleLineForm(forms.Form):
|
||||
country = LazyTypedChoiceField(
|
||||
choices=CountriesAndEU(),
|
||||
required=False
|
||||
)
|
||||
address_type = forms.ChoiceField(
|
||||
choices=[
|
||||
('', _('Any customer')),
|
||||
('individual', _('Individual')),
|
||||
('business', _('Business')),
|
||||
('business_vat_id', _('Business with valid VAT ID')),
|
||||
],
|
||||
required=False
|
||||
)
|
||||
action = forms.ChoiceField(
|
||||
choices=[
|
||||
('vat', _('Charge VAT')),
|
||||
('reverse', _('Reverse charge')),
|
||||
('no', _('No VAT')),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
TaxRuleLineFormSet = formset_factory(
|
||||
TaxRuleLineForm,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
)
|
||||
|
||||
|
||||
class TaxRuleForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = TaxRule
|
||||
|
||||
@@ -606,12 +606,23 @@ class VoucherFilterForm(FilterForm):
|
||||
choices=(
|
||||
('', _('All')),
|
||||
('v', _('Valid')),
|
||||
('r', _('Redeemed')),
|
||||
('u', _('Unredeemed')),
|
||||
('r', _('Redeemed at least once')),
|
||||
('f', _('Fully redeemed')),
|
||||
('e', _('Expired')),
|
||||
('c', _('Redeemed and checked in with ticket')),
|
||||
),
|
||||
required=False
|
||||
)
|
||||
qm = forms.ChoiceField(
|
||||
label=_('Quota handling'),
|
||||
choices=(
|
||||
('', _('All')),
|
||||
('b', _('Reserve ticket from quota')),
|
||||
('i', _('Allow to ignore quota')),
|
||||
),
|
||||
required=False
|
||||
)
|
||||
tag = forms.CharField(
|
||||
label=_('Filter by tag'),
|
||||
widget=forms.TextInput(attrs={
|
||||
@@ -633,6 +644,10 @@ class VoucherFilterForm(FilterForm):
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
itemvar = forms.ChoiceField(
|
||||
label=_("Product"),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
@@ -654,6 +669,19 @@ class VoucherFilterForm(FilterForm):
|
||||
elif 'subevent':
|
||||
del self.fields['subevent']
|
||||
|
||||
choices = [('', _('All products'))]
|
||||
for i in self.event.items.prefetch_related('variations').all():
|
||||
variations = list(i.variations.all())
|
||||
if variations:
|
||||
choices.append((str(i.pk), _('{product} – Any variation').format(product=i.name)))
|
||||
for v in variations:
|
||||
choices.append(('%d-%d' % (i.pk, v.pk), '%s – %s' % (i.name, v.value)))
|
||||
else:
|
||||
choices.append((str(i.pk), i.name))
|
||||
for q in self.event.quotas.all():
|
||||
choices.append(('q-%d' % q.pk, _('Any product in quota "{quota}"').format(quota=q)))
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
@@ -665,12 +693,23 @@ class VoucherFilterForm(FilterForm):
|
||||
s = fdata.get('tag').strip()
|
||||
qs = qs.filter(tag__icontains=s)
|
||||
|
||||
if fdata.get('qm'):
|
||||
s = fdata.get('qm')
|
||||
if s == 'b':
|
||||
qs = qs.filter(block_quota=True)
|
||||
elif s == 'i':
|
||||
qs = qs.filter(allow_ignore_quota=True)
|
||||
|
||||
if fdata.get('status'):
|
||||
s = fdata.get('status')
|
||||
if s == 'v':
|
||||
qs = qs.filter(Q(valid_until__isnull=True) | Q(valid_until__gt=now())).filter(redeemed=0)
|
||||
qs = qs.filter(Q(valid_until__isnull=True) | Q(valid_until__gt=now())).filter(redeemed__lt=F('max_usages'))
|
||||
elif s == 'r':
|
||||
qs = qs.filter(redeemed__gt=0)
|
||||
elif s == 'u':
|
||||
qs = qs.filter(redeemed=0)
|
||||
elif s == 'f':
|
||||
qs = qs.filter(redeemed__gte=F('max_usages'))
|
||||
elif s == 'e':
|
||||
qs = qs.filter(Q(valid_until__isnull=False) & Q(valid_until__lt=now())).filter(redeemed=0)
|
||||
elif s == 'c':
|
||||
@@ -681,6 +720,15 @@ class VoucherFilterForm(FilterForm):
|
||||
redeemed__gt=0, has_checkin=True
|
||||
)
|
||||
|
||||
if fdata.get('itemvar'):
|
||||
if fdata.get('itemvar').startswith('q-'):
|
||||
qs = qs.filter(quota_id=fdata.get('itemvar').split('-')[1])
|
||||
elif '-' in fdata.get('itemvar'):
|
||||
qs = qs.filter(item_id=fdata.get('itemvar').split('-')[0],
|
||||
variation_id=fdata.get('itemvar').split('-')[1])
|
||||
else:
|
||||
qs = qs.filter(item_id=fdata.get('itemvar'))
|
||||
|
||||
if fdata.get('subevent'):
|
||||
qs = qs.filter(subevent_id=fdata.get('subevent').pk)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from pretix.base.models import (
|
||||
from pretix.base.models.items import ItemAddOn
|
||||
from pretix.control.forms import SplitDateTimePickerWidget
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.money import change_decimal_field
|
||||
|
||||
|
||||
class CategoryForm(I18nModelForm):
|
||||
@@ -159,6 +160,7 @@ class ItemCreateForm(I18nModelForm):
|
||||
|
||||
self.fields['category'].queryset = self.instance.event.categories.all()
|
||||
self.fields['tax_rule'].queryset = self.instance.event.tax_rules.all()
|
||||
change_decimal_field(self.fields['default_price'], self.instance.event.currency)
|
||||
self.fields['tax_rule'].empty_label = _('No taxation')
|
||||
self.fields['copy_from'] = forms.ModelChoiceField(
|
||||
label=_("Copy product information"),
|
||||
@@ -292,6 +294,7 @@ class ItemUpdateForm(I18nModelForm):
|
||||
'over 65. This ticket includes access to all parts of the event, except the VIP '
|
||||
'area.'
|
||||
)
|
||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||
|
||||
class Meta:
|
||||
model = Item
|
||||
@@ -345,8 +348,29 @@ class ItemVariationsFormSet(I18nFormSet):
|
||||
return False
|
||||
return form.cleaned_data.get(DELETION_FIELD_NAME, False)
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['event'] = self.event
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
self.is_valid()
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
locales=self.locales,
|
||||
event=self.event
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
|
||||
class ItemVariationForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
change_decimal_field(self.fields['default_price'], self.event.currency)
|
||||
|
||||
class Meta:
|
||||
model = ItemVariation
|
||||
localized_fields = '__all__'
|
||||
@@ -399,7 +423,6 @@ class ItemAddOnsFormSet(I18nFormSet):
|
||||
|
||||
class ItemAddOnForm(I18nModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['addon_category'].queryset = self.event.categories.all()
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils.formats import localize
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
@@ -12,6 +11,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.base.services.pricing import get_price
|
||||
from pretix.helpers.money import change_decimal_field
|
||||
|
||||
|
||||
class ExtendForm(I18nModelForm):
|
||||
@@ -62,7 +62,7 @@ class ExporterForm(forms.Form):
|
||||
class CommentForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = ['comment']
|
||||
fields = ['comment', 'checkin_attention']
|
||||
widgets = {
|
||||
'comment': forms.Textarea(attrs={
|
||||
'rows': 3,
|
||||
@@ -76,8 +76,8 @@ class SubEventChoiceField(forms.ModelChoiceField):
|
||||
p = get_price(self.instance.item, self.instance.variation,
|
||||
voucher=self.instance.voucher,
|
||||
subevent=obj)
|
||||
return '{} – {} ({} {})'.format(obj.name, obj.get_date_range_display(),
|
||||
p, self.instance.order.event.currency)
|
||||
return '{} – {} ({})'.format(obj.name, obj.get_date_range_display(),
|
||||
p.print(self.instance.order.event.currency))
|
||||
|
||||
|
||||
class OtherOperationsForm(forms.Form):
|
||||
@@ -120,6 +120,7 @@ class OrderPositionAddForm(forms.Form):
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('Gross price'),
|
||||
help_text=_("Including taxes, if any. Keep empty for the product's default price")
|
||||
)
|
||||
@@ -149,10 +150,10 @@ class OrderPositionAddForm(forms.Form):
|
||||
for v in variations:
|
||||
p = get_price(i, v, invoice_address=ia)
|
||||
choices.append(('%d-%d' % (i.pk, v.pk),
|
||||
'%s – %s (%s %s)' % (pname, v.value, p, order.event.currency)))
|
||||
'%s – %s (%s)' % (pname, v.value, p.print(order.event.currency))))
|
||||
else:
|
||||
p = get_price(i, invoice_address=ia)
|
||||
choices.append((str(i.pk), '%s (%s %s)' % (pname, p, order.event.currency)))
|
||||
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(order.event.currency))))
|
||||
self.fields['itemvar'].choices = choices
|
||||
if ItemAddOn.objects.filter(base_item__event=order.event).exists():
|
||||
self.fields['addon_to'].queryset = order.positions.filter(addon_to__isnull=True).select_related(
|
||||
@@ -165,6 +166,7 @@ class OrderPositionAddForm(forms.Form):
|
||||
self.fields['subevent'].queryset = order.event.subevents.all()
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
change_decimal_field(self.fields['price'], order.event.currency)
|
||||
|
||||
|
||||
class OrderPositionChangeForm(forms.Form):
|
||||
@@ -178,6 +180,7 @@ class OrderPositionChangeForm(forms.Form):
|
||||
price = forms.DecimalField(
|
||||
required=False,
|
||||
max_digits=10, decimal_places=2,
|
||||
localize=True,
|
||||
label=_('New price (gross)')
|
||||
)
|
||||
operation = forms.ChoiceField(
|
||||
@@ -236,14 +239,13 @@ class OrderPositionChangeForm(forms.Form):
|
||||
p = get_price(i, v, voucher=instance.voucher, subevent=instance.subevent,
|
||||
invoice_address=ia)
|
||||
choices.append(('%d-%d' % (i.pk, v.pk),
|
||||
'%s – %s (%s %s)' % (pname, v.value, localize(p),
|
||||
instance.order.event.currency)))
|
||||
'%s – %s (%s)' % (pname, v.value, p.print(instance.order.event.currency))))
|
||||
else:
|
||||
p = get_price(i, None, voucher=instance.voucher, subevent=instance.subevent,
|
||||
invoice_address=ia)
|
||||
choices.append((str(i.pk), '%s (%s %s)' % (pname, localize(p),
|
||||
instance.order.event.currency)))
|
||||
choices.append((str(i.pk), '%s (%s)' % (pname, p.print(instance.order.event.currency))))
|
||||
self.fields['itemvar'].choices = choices
|
||||
change_decimal_field(self.fields['price'], instance.order.event.currency)
|
||||
|
||||
def clean(self):
|
||||
if self.cleaned_data.get('operation') == 'price' and not self.cleaned_data.get('price', '') != '':
|
||||
|
||||
@@ -5,7 +5,9 @@ from i18nfield.forms import I18nInlineFormSet
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import SubEventItem
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.forms import SplitDateTimePickerWidget
|
||||
from pretix.helpers.money import change_decimal_field
|
||||
|
||||
|
||||
class SubEventForm(I18nModelForm):
|
||||
@@ -49,32 +51,35 @@ class SubEventItemOrVariationFormMixin:
|
||||
self.item = kwargs.pop('item')
|
||||
self.variation = kwargs.pop('variation', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
change_decimal_field(self.fields['price'], self.item.event.currency)
|
||||
|
||||
|
||||
class SubEventItemForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format(
|
||||
self.item.default_price, self.item.event.currency
|
||||
)
|
||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.item.default_price, self.item.event.currency, hide_currency=True)
|
||||
self.fields['price'].label = str(self.item.name)
|
||||
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ['price']
|
||||
widgets = {
|
||||
'price': forms.TextInput
|
||||
}
|
||||
|
||||
|
||||
class SubEventItemVariationForm(SubEventItemOrVariationFormMixin, forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['price'].widget.attrs['placeholder'] = '{} {}'.format(
|
||||
self.variation.price, self.item.event.currency
|
||||
)
|
||||
self.fields['price'].widget.attrs['placeholder'] = money_filter(self.variation.price, self.item.event.currency, hide_currency=True)
|
||||
self.fields['price'].label = '{} – {}'.format(str(self.item.name), self.variation.value)
|
||||
|
||||
class Meta:
|
||||
model = SubEventItem
|
||||
fields = ['price']
|
||||
widgets = {
|
||||
'price': forms.TextInput
|
||||
}
|
||||
|
||||
|
||||
class QuotaFormSet(I18nInlineFormSet):
|
||||
|
||||
@@ -179,6 +179,6 @@ class VoucherBulkForm(VoucherForm):
|
||||
data['code'] = code
|
||||
data['bulk'] = True
|
||||
del data['codes']
|
||||
obj.save()
|
||||
objs.append(obj)
|
||||
Voucher.objects.bulk_create(objs)
|
||||
return objs
|
||||
|
||||
@@ -4,7 +4,6 @@ from decimal import Decimal
|
||||
import dateutil.parser
|
||||
import pytz
|
||||
from django.dispatch import receiver
|
||||
from django.utils import formats
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.strings import LazyI18nString
|
||||
@@ -13,6 +12,7 @@ from pretix.base.models import (
|
||||
CheckinList, Event, ItemVariation, LogEntry, OrderPosition,
|
||||
)
|
||||
from pretix.base.signals import logentry_display
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
|
||||
OVERVIEW_BLACKLIST = [
|
||||
'pretix.plugins.sendmail.order.email.sent'
|
||||
@@ -30,42 +30,38 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
new_item = str(event.items.get(pk=data['new_item']))
|
||||
if data['new_variation']:
|
||||
new_item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['new_variation']))
|
||||
return text + ' ' + _('Position #{posid}: {old_item} ({old_price} {currency}) changed '
|
||||
'to {new_item} ({new_price} {currency}).').format(
|
||||
return text + ' ' + _('Position #{posid}: {old_item} ({old_price}) changed '
|
||||
'to {new_item} ({new_price}).').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_item=old_item, new_item=new_item,
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
new_price=formats.localize(Decimal(data['new_price'])),
|
||||
currency=event.currency
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
new_price=money_filter(Decimal(data['new_price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.subevent':
|
||||
old_se = str(event.subevents.get(pk=data['old_subevent']))
|
||||
new_se = str(event.subevents.get(pk=data['new_subevent']))
|
||||
return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price} {currency}) changed '
|
||||
'to "{new_event}" ({new_price} {currency}).').format(
|
||||
return text + ' ' + _('Position #{posid}: Event date "{old_event}" ({old_price}) changed '
|
||||
'to "{new_event}" ({new_price}).').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_event=old_se, new_event=new_se,
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
new_price=formats.localize(Decimal(data['new_price'])),
|
||||
currency=event.currency
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
new_price=money_filter(Decimal(data['new_price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.price':
|
||||
return text + ' ' + _('Price of position #{posid} changed from {old_price} {currency} '
|
||||
'to {new_price} {currency}.').format(
|
||||
return text + ' ' + _('Price of position #{posid} changed from {old_price} '
|
||||
'to {new_price}.').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
new_price=formats.localize(Decimal(data['new_price'])),
|
||||
currency=event.currency
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
new_price=money_filter(Decimal(data['new_price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.cancel':
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
|
||||
return text + ' ' + _('Position #{posid} ({old_item}, {old_price} {currency}) removed.').format(
|
||||
return text + ' ' + _('Position #{posid} ({old_item}, {old_price}) removed.').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
old_item=old_item,
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
currency=event.currency
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.add':
|
||||
item = str(event.items.get(pk=data['item']))
|
||||
@@ -73,30 +69,27 @@ def _display_order_changed(event: Event, logentry: LogEntry):
|
||||
item += ' - ' + str(ItemVariation.objects.get(item__event=event, pk=data['variation']))
|
||||
if data['addon_to']:
|
||||
addon_to = OrderPosition.objects.get(order__event=event, pk=data['addon_to'])
|
||||
return text + ' ' + _('Position #{posid} created: {item} ({price} {currency}) as an add-on to '
|
||||
return text + ' ' + _('Position #{posid} created: {item} ({price}) as an add-on to '
|
||||
'position #{addon_to}.').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
item=item, addon_to=addon_to.positionid,
|
||||
price=formats.localize(Decimal(data['price'])),
|
||||
currency=event.currency
|
||||
price=money_filter(Decimal(data['price']), event.currency),
|
||||
)
|
||||
else:
|
||||
return text + ' ' + _('Position #{posid} created: {item} ({price} {currency}).').format(
|
||||
return text + ' ' + _('Position #{posid} created: {item} ({price}).').format(
|
||||
posid=data.get('positionid', '?'),
|
||||
item=item,
|
||||
price=formats.localize(Decimal(data['price'])),
|
||||
currency=event.currency
|
||||
price=money_filter(Decimal(data['price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.split':
|
||||
old_item = str(event.items.get(pk=data['old_item']))
|
||||
if data['old_variation']:
|
||||
old_item += ' - ' + str(ItemVariation.objects.get(pk=data['old_variation']))
|
||||
return text + ' ' + _('Position #{posid} ({old_item}, {old_price} {currency}) split into new order: {order}').format(
|
||||
return text + ' ' + _('Position #{posid} ({old_item}, {old_price}) split into new order: {order}').format(
|
||||
old_item=old_item,
|
||||
posid=data.get('positionid', '?'),
|
||||
order=data['new_order'],
|
||||
old_price=formats.localize(Decimal(data['old_price'])),
|
||||
currency=event.currency
|
||||
old_price=money_filter(Decimal(data['old_price']), event.currency),
|
||||
)
|
||||
elif logentry.action_type == 'pretix.event.order.changed.split_from':
|
||||
return _('This order has been created by splitting the order {order}').format(
|
||||
@@ -124,6 +117,8 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.invoice.regenerated': _('The invoice has been regenerated.'),
|
||||
'pretix.event.order.invoice.reissued': _('The invoice has been reissued.'),
|
||||
'pretix.event.order.comment': _('The order\'s internal comment has been updated.'),
|
||||
'pretix.event.order.checkin_attention': _('The order\'s flag to require attention at check-in has been '
|
||||
'toggled.'),
|
||||
'pretix.event.order.payment.changed': _('The payment method has been changed.'),
|
||||
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
|
||||
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import time
|
||||
from urllib.parse import quote, urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
@@ -11,6 +10,9 @@ from django.utils.encoding import force_str
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
)
|
||||
|
||||
|
||||
class PermissionMiddleware(MiddlewareMixin):
|
||||
@@ -64,18 +66,15 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
if not request.user.is_authenticated:
|
||||
return self._login_redirect(request)
|
||||
|
||||
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
|
||||
try:
|
||||
# If this logic is updated, make sure to also update the logic in pretix/api/auth/permission.py
|
||||
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 self._login_redirect(request)
|
||||
assert_session_valid(request)
|
||||
except SessionInvalid:
|
||||
logout(request)
|
||||
return self._login_redirect(request)
|
||||
except SessionReauthRequired:
|
||||
if url_name != 'user.reauth':
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
request.session['pretix_auth_last_used'] = int(time.time())
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
request.event = Event.objects.filter(
|
||||
|
||||
@@ -86,6 +86,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=e.order.code %}">{{ e.order.code }}</a></strong>
|
||||
{% if e.order.status == "n" %}
|
||||
<span class="label label-warning">{% trans "unpaid" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ e.item.name }}{% if e.variation %} – {{ e.variation }}{% endif %}</td>
|
||||
<td>{{ e.order.email }}</td>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
<legend>{% trans "Products" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "pretixcontrol/event/settings_base.html" %}
|
||||
{% load i18n %}
|
||||
{% load formset_tags %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}
|
||||
{% if rule %}
|
||||
@@ -21,7 +22,7 @@
|
||||
{% bootstrap_field form.rate addon_after="%" layout="control" %}
|
||||
<legend>{% trans "Advanced settings" %}</legend>
|
||||
<div class="alert alert-warning">
|
||||
<span class="fa fa-w fa-legal fa-4x pull-left"></span>
|
||||
<span class="fa fa-fw fa-legal fa-4x pull-left"></span>
|
||||
{% blocktrans trimmed with docs="https://docs.pretix.eu/en/latest/user/events/taxes.html" %}
|
||||
These settings are intended for advanced users. See the <a href="{{ docs }}">documentation</a>
|
||||
for more information. Note that we are not responsible for the correct handling
|
||||
@@ -32,6 +33,75 @@
|
||||
{% bootstrap_field form.price_includes_tax layout="control" %}
|
||||
{% bootstrap_field form.eu_reverse_charge layout="control" %}
|
||||
{% bootstrap_field form.home_country layout="control" %}
|
||||
<legend>{% trans "Custom taxation rules" %}</legend>
|
||||
<div class="alert alert-warning">
|
||||
<span class="fa fa-fw fa-exclamation-circle fa-4x pull-left"></span>
|
||||
{% blocktrans trimmed %}
|
||||
These settings are intended for professional users with very specific taxation situations.
|
||||
If you create any rule here, the reverse charge settings above will be ignored. The rules will be
|
||||
checked in order and once the first rule matches the order, it will be used and all further rules will
|
||||
be ignored. If no rule matches, tax will be charged.
|
||||
{% endblocktrans %}
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field form.country layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
{% bootstrap_field form.address_type layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
{% bootstrap_field form.action layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-2 text-right">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="row" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{% bootstrap_field formset.empty_form.country layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
{% bootstrap_field formset.empty_form.address_type layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
{% bootstrap_field formset.empty_form.action layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-sm-2 text-right">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new rule" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
@@ -57,6 +58,10 @@
|
||||
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
@@ -70,12 +75,17 @@
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right">
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
{% load safelink %}
|
||||
{% load eventsignal %}
|
||||
{% block title %}
|
||||
@@ -93,21 +94,23 @@
|
||||
{% endif %}
|
||||
<dt>{% trans "User" %}</dt>
|
||||
<dd>
|
||||
{{ order.email }}
|
||||
{{ order.email|default_if_none:"" }}
|
||||
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-edit"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.order.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
{% if order.status != "c" %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% if order.email %}
|
||||
<a href="{% url "control:event.order.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
{% if order.status != "c" %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% if invoices %}
|
||||
@@ -255,7 +258,7 @@
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 price">
|
||||
{% if event.settings.display_net_prices %}
|
||||
<strong>{{ event.currency }} {{ line.net_price|floatformat:2 }}</strong>
|
||||
<strong>{{ line.net_price|money:event.currency }}</strong>
|
||||
{% if line.tax_rate %}
|
||||
<br />
|
||||
<small>
|
||||
@@ -265,7 +268,7 @@
|
||||
</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<strong>{{ event.currency }} {{ line.price|floatformat:2 }}</strong>
|
||||
<strong>{{ line.price|money:event.currency }}</strong>
|
||||
{% if line.tax_rate and line.price %}
|
||||
<br />
|
||||
<small>
|
||||
@@ -289,7 +292,7 @@
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
|
||||
{% if event.settings.display_net_prices %}
|
||||
<strong>{{ event.currency }} {{ fee.net_value|floatformat:2 }}</strong>
|
||||
<strong>{{ fee.net_value|money:event.currency }}</strong>
|
||||
{% if fee.tax_rate %}
|
||||
<br/>
|
||||
<small>
|
||||
@@ -299,7 +302,7 @@
|
||||
</small>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<strong>{{ event.currency }} {{ fee.value|floatformat:2 }}</strong>
|
||||
<strong>{{ fee.value|money:event.currency }}</strong>
|
||||
{% if fee.tax_rate %}
|
||||
<br/>
|
||||
<small>
|
||||
@@ -319,7 +322,7 @@
|
||||
<strong>{% trans "Net total" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
|
||||
{{ event.currency }} {{ items.net_total|floatformat:2 }}
|
||||
{{ items.net_total|money:event.currency }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -328,7 +331,7 @@
|
||||
<strong>{% trans "Taxes" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
|
||||
{{ event.currency }} {{ items.tax_total|floatformat:2 }}
|
||||
{{ items.tax_total|money:event.currency }}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -338,7 +341,7 @@
|
||||
<strong>{% trans "Total" %}</strong>
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
|
||||
<strong>{{ event.currency }} {{ items.total|floatformat:2 }}</strong>
|
||||
<strong>{{ items.total|money:event.currency }}</strong>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -433,6 +436,7 @@
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
{% bootstrap_field comment_form.comment layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
|
||||
{% bootstrap_field comment_form.checkin_attention layout="horizontal" show_help=True show_label=False horizontal_field_class="col-md-12" %}
|
||||
</div>
|
||||
<button class="btn btn-default">
|
||||
{% trans "Update comment" %}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Orders" %}{% endblock %}
|
||||
{% block content %}
|
||||
@@ -108,13 +109,13 @@
|
||||
</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ o.email }}
|
||||
{{ o.email|default_if_none:"" }}
|
||||
{% if o.invoice_address.name %}
|
||||
<br>{{ o.invoice_address.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td class="text-right">{{ o.total|floatformat:2 }} {{ request.event.currency }}</td>
|
||||
<td class="text-right">{{ o.total|money:request.event.currency }}</td>
|
||||
<td class="text-right">{{ o.pcnt }}</td>
|
||||
<td class="text-right">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
</tr>
|
||||
|
||||
@@ -50,12 +50,12 @@
|
||||
{% if tup.0 %}
|
||||
<tr class="category">
|
||||
<th>{{ tup.0.name }}</th>
|
||||
<th>{{ tup.0.num_canceled|togglesum }}</th>
|
||||
<th>{{ tup.0.num_refunded|togglesum }}</th>
|
||||
<th>{{ tup.0.num_expired|togglesum }}</th>
|
||||
<th>{{ tup.0.num_pending|togglesum }}</th>
|
||||
<th>{{ tup.0.num_paid|togglesum }}</th>
|
||||
<th>{{ tup.0.num_total|togglesum }}</th>
|
||||
<th>{{ tup.0.num_canceled|togglesum:request.event.currency }}</th>
|
||||
<th>{{ tup.0.num_refunded|togglesum:request.event.currency }}</th>
|
||||
<th>{{ tup.0.num_expired|togglesum:request.event.currency }}</th>
|
||||
<th>{{ tup.0.num_pending|togglesum:request.event.currency }}</th>
|
||||
<th>{{ tup.0.num_paid|togglesum:request.event.currency }}</th>
|
||||
<th>{{ tup.0.num_total|togglesum:request.event.currency }}</th>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for item in tup.1 %}
|
||||
@@ -63,43 +63,43 @@
|
||||
<td>{{ item.name }}</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}&status=c&provider={{ item.provider }}">
|
||||
{{ item.num_canceled|togglesum }}
|
||||
{{ item.num_canceled|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}&status=r&provider={{ item.provider }}">
|
||||
{{ item.num_refunded|togglesum }}
|
||||
{{ item.num_refunded|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}&status=e&provider={{ item.provider }}">
|
||||
{{ item.num_expired|togglesum }}
|
||||
{{ item.num_expired|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}&status=n&provider={{ item.provider }}">
|
||||
{{ item.num_pending|togglesum }}
|
||||
{{ item.num_pending|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ listurl }}?item={{ item.id }}&status=p&provider={{ item.provider }}">
|
||||
{{ item.num_paid|togglesum }}
|
||||
{{ item.num_paid|togglesum:request.event.currency }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ item.num_total|togglesum }}
|
||||
{{ item.num_total|togglesum:request.event.currency }}
|
||||
</td>
|
||||
</tr>
|
||||
{% if item.has_variations %}
|
||||
{% for var in item.all_variations %}
|
||||
<tr class="variation {% if tup.0 %}categorized{% endif %}">
|
||||
<td>{{ var }}</td>
|
||||
<td>{{ var.num_canceled|togglesum }}</td>
|
||||
<td>{{ var.num_refunded|togglesum }}</td>
|
||||
<td>{{ var.num_expired|togglesum }}</td>
|
||||
<td>{{ var.num_pending|togglesum }}</td>
|
||||
<td>{{ var.num_paid|togglesum }}</td>
|
||||
<td>{{ var.num_total|togglesum }}</td>
|
||||
<td>{{ var.num_canceled|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num_refunded|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num_expired|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num_pending|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num_paid|togglesum:request.event.currency }}</td>
|
||||
<td>{{ var.num_total|togglesum:request.event.currency }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -109,12 +109,12 @@
|
||||
<tfoot>
|
||||
<tr class="total">
|
||||
<th>{% trans "Total" %}</th>
|
||||
<th>{{ total.num_canceled|togglesum }}</th>
|
||||
<th>{{ total.num_refunded|togglesum }}</th>
|
||||
<th>{{ total.num_expired|togglesum }}</th>
|
||||
<th>{{ total.num_pending|togglesum }}</th>
|
||||
<th>{{ total.num_paid|togglesum }}</th>
|
||||
<th>{{ total.num_total|togglesum }}</th>
|
||||
<th>{{ total.num_canceled|togglesum:request.event.currency }}</th>
|
||||
<th>{{ total.num_refunded|togglesum:request.event.currency }}</th>
|
||||
<th>{{ total.num_expired|togglesum:request.event.currency }}</th>
|
||||
<th>{{ total.num_pending|togglesum:request.event.currency }}</th>
|
||||
<th>{{ total.num_paid|togglesum:request.event.currency }}</th>
|
||||
<th>{{ total.num_total|togglesum:request.event.currency }}</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Order search" %}{% endblock %}
|
||||
{% block content %}
|
||||
@@ -64,13 +65,13 @@
|
||||
</td>
|
||||
<td>{{ o.event.name }}</td>
|
||||
<td>
|
||||
{{ o.email }}
|
||||
{{ o.email|default_if_none:"" }}
|
||||
{% if o.invoice_address.name %}
|
||||
<br>{{ o.invoice_address.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
<td class="text-right">{{ o.total|floatformat:2 }} {{ o.event.currency }}</td>
|
||||
<td class="text-right">{{ o.total|money:o.event.currency }}</td>
|
||||
<td class="text-right">{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Item prices" %}</legend>
|
||||
{% for f in itemvar_forms %}
|
||||
{% bootstrap_field f.price layout="control" %}
|
||||
{% bootstrap_field f.price addon_after=request.event.currency layout="control" %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
@@ -150,6 +150,7 @@
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
</div>
|
||||
@@ -177,6 +178,7 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||
</div>
|
||||
|
||||
@@ -32,16 +32,16 @@
|
||||
<fieldset>
|
||||
<legend>{% trans "Choose event" %}</legend>
|
||||
<p>
|
||||
<select name="event" class="form-control">
|
||||
<option value="">{% trans "All my events" %}</option>
|
||||
{% for e in events %}
|
||||
<option value="{{ e.pk }}"
|
||||
{% if e.pk|floatformat:0 == request.GET.event %}selected="selected"{% endif %}>
|
||||
{{ e.name }} – {{ e.get_date_range_display }}
|
||||
<select name="event" class="form-control simple-subevent-choice"
|
||||
data-model-select2="event"
|
||||
data-placeholder="{% trans "All my events" %}"
|
||||
data-select2-url="{% url "control:events.typeahead" %}">
|
||||
{% if event %}
|
||||
<option value="{{ event.pk }}" selected>
|
||||
{{ event.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Choose" %}</button>
|
||||
<span class="help-block">{% trans "Save your modifications before switching events." %}</span>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
@@ -12,24 +12,30 @@
|
||||
</p>
|
||||
<div class="row filter-form">
|
||||
<form class="" action="" method="get">
|
||||
<div class="col-md-3 col-xs-6">
|
||||
<div class="col-md-2 col-xs-6">
|
||||
{% bootstrap_field filter_form.search layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-3 col-xs-6">
|
||||
<div class="col-md-2 col-xs-6">
|
||||
{% bootstrap_field filter_form.tag layout='inline' %}
|
||||
</div>
|
||||
{% if request.event.has_subevents %}
|
||||
<div class="col-md-2 col-xs-6">
|
||||
<div class="col-md-1 col-xs-6">
|
||||
{% bootstrap_field filter_form.status layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6">
|
||||
{% bootstrap_field filter_form.subevent layout='inline' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-md-4 col-xs-6">
|
||||
<div class="col-md-3 col-xs-6">
|
||||
{% bootstrap_field filter_form.status layout='inline' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-2 col-xs-6">
|
||||
{% bootstrap_field filter_form.itemvar layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-1 col-xs-6">
|
||||
{% bootstrap_field filter_form.qm layout='inline' %}
|
||||
</div>
|
||||
<div class="col-md-2 col-xs-6">
|
||||
<button class="btn btn-primary btn-block" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load money %}
|
||||
{% load urlreplace %}
|
||||
{% block title %}{% trans "Waiting list" %}{% endblock %}
|
||||
{% block content %}
|
||||
@@ -65,9 +66,9 @@
|
||||
{% trans "Sales estimate" %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% blocktrans trimmed with amount=estimate|default:0|floatformat:2 currency=request.event.currency %}
|
||||
{% blocktrans trimmed with amount=estimate|default:0|money:request.event.currency %}
|
||||
If you can make enough room at your event to fit all the persons on the waiting list in, you
|
||||
could sell tickets worth an additional <strong>{{ amount }} {{ currency }}</strong>.
|
||||
could sell tickets worth an additional <strong>{{ amount }}</strong>.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django import template
|
||||
from django.utils import formats
|
||||
from django.conf import settings
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
@@ -7,7 +8,7 @@ register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name='togglesum', needs_autoescape=True)
|
||||
def cut(value, autoescape=True):
|
||||
def togglesum_filter(value, arg='EUR', autoescape=True):
|
||||
def noop(x):
|
||||
return x
|
||||
|
||||
@@ -17,6 +18,10 @@ def cut(value, autoescape=True):
|
||||
esc = conditional_escape
|
||||
else:
|
||||
esc = noop
|
||||
|
||||
places = settings.CURRENCY_PLACES.get(arg, 2)
|
||||
return mark_safe('<span class="count">{0}</span><span class="sum-gross">{1}</span><span class="sum-net">{2}</span>'.format(
|
||||
esc(value[0]), esc(formats.localize(value[1])), esc(formats.localize(value[2]))
|
||||
esc(value[0]),
|
||||
esc(floatformat(value[1], places)),
|
||||
esc(floatformat(value[2], places))
|
||||
))
|
||||
|
||||
@@ -48,7 +48,7 @@ def login(request):
|
||||
request.session['pretix_auth_2fa_user'] = form.user_cache.pk
|
||||
request.session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||
twofa_url = reverse('control:auth.login.2fa')
|
||||
if 'next' in request.GET:
|
||||
if "next" in request.GET and is_safe_url(request.GET.get("next")):
|
||||
twofa_url += '?next=' + quote(request.GET.get('next'))
|
||||
return redirect(twofa_url)
|
||||
else:
|
||||
@@ -71,7 +71,10 @@ def logout(request):
|
||||
"""
|
||||
auth_logout(request)
|
||||
request.session['pretix_auth_login_time'] = 0
|
||||
return redirect('control:auth.login')
|
||||
next = reverse('control:auth.login')
|
||||
if 'next' in request.GET and is_safe_url(request.GET.get('next')):
|
||||
next += '?next=' + quote(request.GET.get('next'))
|
||||
return redirect(next)
|
||||
|
||||
|
||||
def register(request):
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.db.models import Max, OuterRef, Subquery
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.timezone import is_aware, make_aware, now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import DeleteView, ListView
|
||||
from pytz import UTC
|
||||
@@ -35,7 +35,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.request.event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
order__status__in=[Order.STATUS_PAID, Order.STATUS_PENDING] if self.list.include_pending else [Order.STATUS_PAID],
|
||||
subevent=self.list.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
@@ -70,7 +70,11 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
if isinstance(e.last_checked_in, str):
|
||||
# Apparently only happens on SQLite
|
||||
e.last_checked_in_aware = make_aware(dateutil.parser.parse(e.last_checked_in), UTC)
|
||||
elif not is_aware(e.last_checked_in):
|
||||
# Apparently only happens on MySQL
|
||||
e.last_checked_in_aware = make_aware(e.last_checked_in, UTC)
|
||||
else:
|
||||
# This would be correct, so guess on which database it works… Yes, it's PostgreSQL.
|
||||
e.last_checked_in_aware = e.last_checked_in
|
||||
return ctx
|
||||
|
||||
@@ -88,7 +92,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
|
||||
for op in positions:
|
||||
created = False
|
||||
if op.order.status == Order.STATUS_PAID:
|
||||
if op.order.status == Order.STATUS_PAID or (self.list.include_pending and op.order.status == Order.STATUS_PENDING):
|
||||
ci, created = Checkin.objects.get_or_create(position=op, list=self.list, defaults={
|
||||
'datetime': now(),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import json
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from django.conf import settings
|
||||
@@ -26,6 +28,7 @@ from django.views.generic.detail import SingleObjectMixin
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from pytz import timezone
|
||||
|
||||
from pretix.base.i18n import LazyCurrencyNumber
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedTicket, Event, Item, ItemVariation, LogEntry,
|
||||
Order, RequiredAction, TaxRule, Voucher,
|
||||
@@ -34,11 +37,12 @@ from pretix.base.models.event import EventMetaValue
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.invoices import build_preview_invoice_pdf
|
||||
from pretix.base.signals import event_live_issues, register_ticket_outputs
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.forms.event import (
|
||||
CommentForm, DisplaySettingsForm, EventDeleteForm, EventMetaValueForm,
|
||||
EventSettingsForm, EventUpdateForm, InvoiceSettingsForm, MailSettingsForm,
|
||||
PaymentSettingsForm, ProviderForm, TaxRuleForm, TicketSettingsForm,
|
||||
WidgetCodeForm,
|
||||
PaymentSettingsForm, ProviderForm, TaxRuleForm, TaxRuleLineFormSet,
|
||||
TicketSettingsForm, WidgetCodeForm,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.signals import nav_event_settings
|
||||
@@ -492,8 +496,8 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
return {
|
||||
'date': date_format(now() + timedelta(days=7), 'SHORT_DATE_FORMAT'),
|
||||
'expire_date': date_format(now() + timedelta(days=15), 'SHORT_DATE_FORMAT'),
|
||||
'payment_info': _('{} {} has been transferred to account <9999-9999-9999-9999> at {}').format(
|
||||
42.23, self.request.event.currency, date_format(now(), 'SHORT_DATETIME_FORMAT'))
|
||||
'payment_info': _('{} has been transferred to account <9999-9999-9999-9999> at {}').format(
|
||||
money_filter(Decimal('42.23'), self.request.event.currency), date_format(now(), 'SHORT_DATETIME_FORMAT'))
|
||||
}
|
||||
|
||||
# create index-language mapping
|
||||
@@ -508,7 +512,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
@cached_property
|
||||
def items(self):
|
||||
return {
|
||||
'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company',
|
||||
'mail_text_order_placed': ['total', 'currency', 'date', 'invoice_company', 'total_with_currency',
|
||||
'event', 'payment_info', 'url', 'invoice_name'],
|
||||
'mail_text_order_paid': ['event', 'url', 'invoice_name', 'invoice_company', 'payment_info'],
|
||||
'mail_text_order_free': ['event', 'url', 'invoice_name', 'invoice_company'],
|
||||
@@ -536,6 +540,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
return {
|
||||
'event': self.request.event.name,
|
||||
'total': 42.23,
|
||||
'total_with_currency': LazyCurrencyNumber(42.23, self.request.event.currency),
|
||||
'currency': self.request.event.currency,
|
||||
'url': self.generate_order_url(user_orders[0]['code'], user_orders[0]['secret']),
|
||||
'orders': '\n'.join(orders),
|
||||
@@ -948,9 +953,30 @@ class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView
|
||||
'name': LazyI18nString.from_gettext(ugettext('VAT'))
|
||||
}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid() and self.formset.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
return TaxRuleLineFormSet(
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
return ctx
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
form.instance.event = self.request.event
|
||||
form.instance.custom_rules = json.dumps([
|
||||
f.cleaned_data for f in self.formset if f not in self.formset.deleted_forms
|
||||
])
|
||||
messages.success(self.request, _('The new tax rule has been created.'))
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.event.taxrule.added', user=self.request.user, data=dict(form.cleaned_data))
|
||||
@@ -976,9 +1002,32 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView
|
||||
except TaxRule.DoesNotExist:
|
||||
raise Http404(_("The requested tax rule does not exist."))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object(self.get_queryset())
|
||||
form = self.get_form()
|
||||
if form.is_valid() and self.formset.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
@cached_property
|
||||
def formset(self):
|
||||
return TaxRuleLineFormSet(
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
initial=json.loads(self.object.custom_rules) if self.object.custom_rules else []
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['formset'] = self.formset
|
||||
return ctx
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
form.instance.custom_rules = json.dumps([
|
||||
f.cleaned_data for f in self.formset if f not in self.formset.deleted_forms
|
||||
])
|
||||
if form.has_changed():
|
||||
self.object.log_action(
|
||||
'pretix.event.taxrule.changed', user=self.request.user, data={
|
||||
|
||||
@@ -338,7 +338,7 @@ class QuestionMixin:
|
||||
formsetclass = inlineformset_factory(
|
||||
Question, QuestionOption,
|
||||
form=QuestionOptionForm, formset=I18nFormSet,
|
||||
can_order=False, can_delete=True, extra=0
|
||||
can_order=True, can_delete=True, extra=0
|
||||
)
|
||||
return formsetclass(self.request.POST if self.request.method == "POST" else None,
|
||||
queryset=(QuestionOption.objects.filter(question=self.object)
|
||||
@@ -358,30 +358,25 @@ class QuestionMixin:
|
||||
)
|
||||
form.instance.delete()
|
||||
form.instance.pk = None
|
||||
elif form.has_changed():
|
||||
form.instance.question = obj
|
||||
form.save()
|
||||
|
||||
forms = self.formset.ordered_forms + [
|
||||
ef for ef in self.formset.extra_forms
|
||||
if ef not in self.formset.ordered_forms and ef not in self.formset.deleted_forms
|
||||
]
|
||||
for i, form in enumerate(forms):
|
||||
form.instance.position = i
|
||||
form.instance.question = obj
|
||||
created = not form.instance.pk
|
||||
form.save()
|
||||
if form.has_changed():
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
obj.log_action(
|
||||
'pretix.event.question.option.added' if created else
|
||||
'pretix.event.question.option.changed',
|
||||
user=self.request.user, data=change_data
|
||||
)
|
||||
|
||||
for form in self.formset.extra_forms:
|
||||
if not form.has_changed():
|
||||
continue
|
||||
if self.formset._should_delete_form(form):
|
||||
continue
|
||||
form.instance.question = obj
|
||||
form.save()
|
||||
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||
change_data['id'] = form.instance.pk
|
||||
obj.log_action(
|
||||
'pretix.event.question.option.added',
|
||||
user=self.request.user, data=change_data
|
||||
)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class OrderView(EventPermissionRequiredMixin, DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['can_generate_invoice'] = invoice_qualified(self.order) and (
|
||||
self.request.event.settings.invoice_generate in ('admin', 'user', 'paid')
|
||||
self.request.event.settings.invoice_generate in ('admin', 'user', 'paid', 'True')
|
||||
)
|
||||
return ctx
|
||||
|
||||
@@ -136,7 +136,10 @@ class OrderDetail(OrderView):
|
||||
ctx['event'] = self.request.event
|
||||
ctx['payment'] = self.payment_provider.order_control_render(self.request, self.object)
|
||||
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
||||
ctx['comment_form'] = CommentForm(initial={'comment': self.order.comment})
|
||||
ctx['comment_form'] = CommentForm(initial={
|
||||
'comment': self.order.comment,
|
||||
'checkin_attention': self.order.checkin_attention
|
||||
})
|
||||
ctx['display_locale'] = dict(settings.LANGUAGES)[self.object.locale or self.request.event.settings.locale]
|
||||
return ctx
|
||||
|
||||
@@ -191,11 +194,18 @@ class OrderComment(OrderView):
|
||||
def post(self, *args, **kwargs):
|
||||
form = CommentForm(self.request.POST)
|
||||
if form.is_valid():
|
||||
self.order.comment = form.cleaned_data.get('comment')
|
||||
if form.cleaned_data.get('comment') != self.order.comment:
|
||||
self.order.comment = form.cleaned_data.get('comment')
|
||||
self.order.log_action('pretix.event.order.comment', user=self.request.user, data={
|
||||
'new_comment': form.cleaned_data.get('comment')
|
||||
})
|
||||
|
||||
if form.cleaned_data.get('checkin_attention') != self.order.checkin_attention:
|
||||
self.order.checkin_attention = form.cleaned_data.get('checkin_attention')
|
||||
self.order.log_action('pretix.event.order.checkin_attention', user=self.request.user, data={
|
||||
'new_value': form.cleaned_data.get('checkin_attention')
|
||||
})
|
||||
self.order.save()
|
||||
self.order.log_action('pretix.event.order.comment', user=self.request.user, data={
|
||||
'new_comment': form.cleaned_data.get('comment')
|
||||
})
|
||||
messages.success(self.request, _('The comment has been updated.'))
|
||||
else:
|
||||
messages.error(self.request, _('Could not update the comment.'))
|
||||
|
||||
@@ -148,6 +148,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
'name': cl.name,
|
||||
'all_products': cl.all_products,
|
||||
'limit_products': cl.limit_products.all(),
|
||||
'include_pending': cl.include_pending,
|
||||
} for cl in self.copy_from.checkinlist_set.prefetch_related('limit_products')
|
||||
]
|
||||
extra = len(kwargs['initial'])
|
||||
@@ -156,6 +157,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
{
|
||||
'name': '',
|
||||
'all_products': True,
|
||||
'include_pending': False,
|
||||
}
|
||||
]
|
||||
extra = 1
|
||||
|
||||
@@ -16,7 +16,7 @@ from django.views.generic import (
|
||||
CreateView, DeleteView, ListView, TemplateView, UpdateView, View,
|
||||
)
|
||||
|
||||
from pretix.base.models import Voucher
|
||||
from pretix.base.models import LogEntry, Voucher
|
||||
from pretix.base.models.vouchers import _generate_random_code
|
||||
from pretix.control.forms.filter import VoucherFilterForm
|
||||
from pretix.control.forms.vouchers import VoucherBulkForm, VoucherForm
|
||||
@@ -244,8 +244,15 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, CreateView):
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
for o in form.save(self.request.event):
|
||||
o.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user)
|
||||
log_entries = []
|
||||
form.save(self.request.event)
|
||||
# We need to query them again as form.save() uses bulk_create which does not fill in .pk values on databases
|
||||
# other than PostgreSQL
|
||||
for v in self.request.event.vouchers.filter(code__in=form.cleaned_data['codes']):
|
||||
log_entries.append(
|
||||
v.log_action('pretix.voucher.added', data=form.cleaned_data, user=self.request.user, save=False)
|
||||
)
|
||||
LogEntry.objects.bulk_create(log_entries)
|
||||
messages.success(self.request, _('The new vouchers have been created.'))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
|
||||
34
src/pretix/helpers/money.py
Normal file
34
src/pretix/helpers/money.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.validators import DecimalValidator
|
||||
from django.forms import NumberInput, TextInput
|
||||
from django.utils import formats
|
||||
|
||||
|
||||
class DecimalTextInput(TextInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.places = kwargs.pop('places', 2)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def format_value(self, value):
|
||||
"""
|
||||
Return a value as it should appear when rendered in a template.
|
||||
"""
|
||||
if value == '' or value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return formats.localize_input(value.quantize(Decimal('1') / 10 ** self.places))
|
||||
|
||||
|
||||
def change_decimal_field(field, currency):
|
||||
places = settings.CURRENCY_PLACES.get(currency, 2)
|
||||
field.decimal_places = places
|
||||
if isinstance(field.widget, NumberInput):
|
||||
field.widget.attrs['step'] = str(Decimal('1') / 10 ** places).lower()
|
||||
elif isinstance(field.widget, TextInput):
|
||||
field.widget = DecimalTextInput(places=places)
|
||||
v = [v for v in field.validators if isinstance(v, DecimalValidator)]
|
||||
if len(v) == 1:
|
||||
v[0].decimal_places = places
|
||||
37
src/pretix/helpers/security.py
Normal file
37
src/pretix/helpers/security.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class SessionInvalid(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SessionReauthRequired(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_user_agent_hash(request):
|
||||
return hashlib.sha256(request.META['HTTP_USER_AGENT'].encode()).hexdigest()
|
||||
|
||||
|
||||
def assert_session_valid(request):
|
||||
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:
|
||||
request.session['pretix_auth_login_time'] = 0
|
||||
raise SessionInvalid()
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE:
|
||||
raise SessionReauthRequired()
|
||||
|
||||
if 'HTTP_USER_AGENT' in request.META:
|
||||
if 'pinned_user_agent' in request.session:
|
||||
if request.session.get('pinned_user_agent') != get_user_agent_hash(request):
|
||||
raise SessionInvalid()
|
||||
else:
|
||||
request.session['pinned_user_agent'] = get_user_agent_hash(request)
|
||||
|
||||
request.session['pretix_auth_last_used'] = int(time.time())
|
||||
return True
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-02-03 15:51+0000\n"
|
||||
"POT-Creation-Date: 2018-03-03 20:06+0000\n"
|
||||
"PO-Revision-Date: 2018-02-03 16:56+0100\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -178,11 +178,11 @@ msgstr "Generiere Nachrichten…"
|
||||
msgid "Unknown error."
|
||||
msgstr "Unbekannter Fehler."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:239
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:242
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:240
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:243
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-02-03 15:51+0000\n"
|
||||
"POT-Creation-Date: 2018-03-03 20:06+0000\n"
|
||||
"PO-Revision-Date: 2018-02-03 16:56+0100\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: \n"
|
||||
@@ -178,11 +178,11 @@ msgstr "Generiere Nachrichten…"
|
||||
msgid "Unknown error."
|
||||
msgstr "Unbekannter Fehler."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:239
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:242
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:240
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:243
|
||||
msgid "None"
|
||||
msgstr "Keine"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ presale_patterns_main = [
|
||||
url(r'', include((locale_patterns + [
|
||||
url(r'^(?P<organizer>[^/]+)/', include(organizer_patterns)),
|
||||
url(r'^(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(event_patterns)),
|
||||
url(r'^$', TemplateView.as_view(template_name='pretixpresale/index.html'))
|
||||
url(r'^$', TemplateView.as_view(template_name='pretixpresale/index.html'), name="index")
|
||||
], 'presale')))
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load i18n %}{% load l10n %}{% blocktrans with bank=details|safe code=order.full_code total=order.total|localize currency=event.currency %}
|
||||
{% load i18n %}{% load l10n %}{% load money %}{% blocktrans with bank=details|safe code=order.full_code total=order.total|money:event.currency %}
|
||||
Please transfer the full amount to the following bank account.
|
||||
|
||||
Reference: {{ code }}
|
||||
Amount: {{ total }} {{ currency }}
|
||||
Amount: {{ total }}
|
||||
{{ bank }}
|
||||
{% endblocktrans %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load l10n %}
|
||||
{% load money %}
|
||||
|
||||
<p>{% blocktrans trimmed %}
|
||||
Please transfer the full amount to the following bank account:
|
||||
@@ -7,6 +8,6 @@
|
||||
|
||||
<address>
|
||||
{{ details|linebreaksbr }}<br />
|
||||
{% trans "Amount:" %} {{ order.total|localize }} {{ event.currency }}<br />
|
||||
{% trans "Amount:" %} {{ order.total|money:event.currency }}<br />
|
||||
<strong>{% trans "Reference code (important):" %} {{ order.full_code }}</strong>
|
||||
</address>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% load money %}
|
||||
{% load staticfiles %}
|
||||
<div class="table-responsive">
|
||||
{% csrf_token %}
|
||||
@@ -85,7 +86,7 @@
|
||||
<td>
|
||||
{% if trans.order %}
|
||||
<a href="{% url "control:event.order" event=trans.order.event.slug organizer=request.organizer.slug code=trans.order.code %}"
|
||||
data-toggle="tooltip" title="{{ trans.order.total|floatformat:2 }} {{ trans.order.event.currency }}">
|
||||
data-toggle="tooltip" title="{{ trans.order.total|money:trans.order.event.currency }}">
|
||||
{% if not request.event %}
|
||||
{{ trans.order.event.slug|upper }}-{{ trans.order.code }}
|
||||
{% else %}
|
||||
|
||||
@@ -18,6 +18,7 @@ from pretix.base.models import Order, Quota
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.base.services.orders import mark_order_paid
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, OrganizerPermissionRequiredMixin,
|
||||
)
|
||||
@@ -147,8 +148,6 @@ class ActionView(View):
|
||||
})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
from django.utils.formats import localize
|
||||
|
||||
u = request.GET.get('query', '')
|
||||
if len(u) < 2:
|
||||
return JsonResponse({'results': []})
|
||||
@@ -178,7 +177,7 @@ class ActionView(View):
|
||||
{
|
||||
'code': o.event.slug.upper() + '-' + o.code,
|
||||
'status': o.get_status_display(),
|
||||
'total': localize(o.total) + ' ' + o.event.currency
|
||||
'total': money_filter(o.total, o.event.currency)
|
||||
} for o in qs
|
||||
]
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ from defusedcsv import csv
|
||||
from django import forms
|
||||
from django.db.models import Max, OuterRef, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import is_aware, make_aware
|
||||
from django.utils.translation import pgettext, ugettext as _, ugettext_lazy
|
||||
from pytz import UTC
|
||||
@@ -15,6 +15,7 @@ from reportlab.platypus import Flowable, Paragraph, Spacer, Table, TableStyle
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.models import Checkin, Order, OrderPosition, Question
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
|
||||
|
||||
@@ -37,12 +38,6 @@ class BaseCheckinList(BaseExporter):
|
||||
label=_('Include QR-code secret'),
|
||||
required=False
|
||||
)),
|
||||
('paid_only',
|
||||
forms.BooleanField(
|
||||
label=_('Only paid orders'),
|
||||
initial=True,
|
||||
required=False
|
||||
)),
|
||||
('sort',
|
||||
forms.ChoiceField(
|
||||
label=_('Sort by'),
|
||||
@@ -182,7 +177,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
|
||||
elif form_data['sort'] == 'code':
|
||||
qs = qs.order_by('order__code')
|
||||
|
||||
if form_data['paid_only']:
|
||||
if not cl.include_pending:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
else:
|
||||
qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING))
|
||||
@@ -206,7 +201,7 @@ class PDFCheckinList(ReportlabExportMixin, BaseCheckinList):
|
||||
op.order.code,
|
||||
name,
|
||||
str(op.item.name) + (" – " + str(op.variation.value) if op.variation else "") + "\n" +
|
||||
self.event.currency + " " + localize(op.price),
|
||||
money_filter(op.price, self.event.currency),
|
||||
]
|
||||
acache = {}
|
||||
for a in op.answers.all():
|
||||
@@ -267,7 +262,7 @@ class CSVCheckinList(BaseCheckinList):
|
||||
headers = [
|
||||
_('Order code'), _('Attendee name'), _('Product'), _('Price'), _('Checked in')
|
||||
]
|
||||
if form_data['paid_only']:
|
||||
if not cl.include_pending:
|
||||
qs = qs.filter(order__status=Order.STATUS_PAID)
|
||||
else:
|
||||
qs = qs.filter(order__status__in=(Order.STATUS_PAID, Order.STATUS_PENDING))
|
||||
@@ -303,7 +298,7 @@ class CSVCheckinList(BaseCheckinList):
|
||||
date_format(last_checked_in.astimezone(self.event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
if last_checked_in else ''
|
||||
]
|
||||
if not form_data['paid_only']:
|
||||
if cl.include_pending:
|
||||
row.append(_('Yes') if op.order.status == Order.STATUS_PAID else _('No'))
|
||||
if form_data['secrets']:
|
||||
row.append(op.secret)
|
||||
@@ -319,4 +314,4 @@ class CSVCheckinList(BaseCheckinList):
|
||||
|
||||
writer.writerow(row)
|
||||
|
||||
return 'checkin.csv', 'text/csv', output.getvalue().encode("utf-8")
|
||||
return '{}_checkin.csv'.format(self.event.slug), 'text/csv', output.getvalue().encode("utf-8")
|
||||
|
||||
@@ -55,6 +55,8 @@ class Paypal(BasePaymentProvider):
|
||||
('client_id',
|
||||
forms.CharField(
|
||||
label=_('Client ID'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
help_text=_('<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>').format(
|
||||
text=_('Click here for a tutorial on how to obtain the required keys'),
|
||||
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html'
|
||||
@@ -63,6 +65,8 @@ class Paypal(BasePaymentProvider):
|
||||
('secret',
|
||||
forms.CharField(
|
||||
label=_('Secret'),
|
||||
max_length=80,
|
||||
min_length=80,
|
||||
))
|
||||
]
|
||||
)
|
||||
|
||||
@@ -122,7 +122,6 @@ class ConfigView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
|
||||
class ApiView(View):
|
||||
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, request, **kwargs):
|
||||
try:
|
||||
@@ -156,7 +155,6 @@ class ApiView(View):
|
||||
|
||||
|
||||
class ApiRedeemView(ApiView):
|
||||
|
||||
def _save_answers(self, op, answers, given_answers):
|
||||
for q, a in given_answers.items():
|
||||
if not a:
|
||||
@@ -193,6 +191,7 @@ class ApiRedeemView(ApiView):
|
||||
def post(self, request, **kwargs):
|
||||
secret = request.POST.get('secret', '!INVALID!')
|
||||
force = request.POST.get('force', 'false') in ('true', 'True')
|
||||
ignore_unpaid = request.POST.get('ignore_unpaid', 'false') in ('true', 'True')
|
||||
nonce = request.POST.get('nonce')
|
||||
response = {
|
||||
'version': API_VERSION
|
||||
@@ -237,23 +236,26 @@ class ApiRedeemView(ApiView):
|
||||
|
||||
self._save_answers(op, answers, given_answers)
|
||||
|
||||
if not self.config.list.all_products and op.item_id not in [i.pk for i in self.config.list.limit_products.all()]:
|
||||
if not self.config.list.all_products and op.item_id not in [i.pk for i in
|
||||
self.config.list.limit_products.all()]:
|
||||
response['status'] = 'error'
|
||||
response['reason'] = 'product'
|
||||
elif not self.config.all_items and op.item_id not in [i.pk for i in self.config.items.all()]:
|
||||
response['status'] = 'error'
|
||||
response['reason'] = 'product'
|
||||
elif op.order.status != Order.STATUS_PAID and not force and not (
|
||||
ignore_unpaid and self.config.list.include_pending and op.order.status == Order.STATUS_PENDING
|
||||
):
|
||||
response['status'] = 'error'
|
||||
response['reason'] = 'unpaid'
|
||||
elif require_answers and not force and request.POST.get('questions_supported'):
|
||||
response['status'] = 'incomplete'
|
||||
response['questions'] = require_answers
|
||||
elif op.order.status == Order.STATUS_PAID or force:
|
||||
else:
|
||||
ci, created = Checkin.objects.get_or_create(position=op, list=self.config.list, defaults={
|
||||
'datetime': dt,
|
||||
'nonce': nonce,
|
||||
})
|
||||
else:
|
||||
response['status'] = 'error'
|
||||
response['reason'] = 'unpaid'
|
||||
|
||||
if 'status' not in response:
|
||||
if created or (nonce and nonce == ci.nonce):
|
||||
@@ -282,7 +284,8 @@ class ApiRedeemView(ApiView):
|
||||
'list': self.config.list.pk
|
||||
})
|
||||
|
||||
response['data'] = serialize_op(op, redeemed=op.order.status == Order.STATUS_PAID or force)
|
||||
response['data'] = serialize_op(op, redeemed=op.order.status == Order.STATUS_PAID or force,
|
||||
clist=self.config.list)
|
||||
|
||||
except OrderPosition.DoesNotExist:
|
||||
response['status'] = 'error'
|
||||
@@ -310,7 +313,7 @@ def serialize_question(q, items=False):
|
||||
return d
|
||||
|
||||
|
||||
def serialize_op(op, redeemed):
|
||||
def serialize_op(op, redeemed, clist):
|
||||
name = op.attendee_name
|
||||
if not name and op.addon_to:
|
||||
name = op.addon_to.attendee_name
|
||||
@@ -319,6 +322,13 @@ def serialize_op(op, redeemed):
|
||||
name = op.order.invoice_address.name
|
||||
except:
|
||||
pass
|
||||
checkin_allowed = (
|
||||
op.order.status == Order.STATUS_PAID
|
||||
or (
|
||||
op.order.status == Order.STATUS_PENDING
|
||||
and clist.include_pending
|
||||
)
|
||||
)
|
||||
return {
|
||||
'secret': op.secret,
|
||||
'order': op.order.code,
|
||||
@@ -327,9 +337,10 @@ def serialize_op(op, redeemed):
|
||||
'variation': str(op.variation) if op.variation else None,
|
||||
'variation_id': op.variation_id,
|
||||
'attendee_name': name,
|
||||
'attention': op.item.checkin_attention,
|
||||
'attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||
'redeemed': redeemed,
|
||||
'paid': op.order.status == Order.STATUS_PAID,
|
||||
'checkin_allowed': checkin_allowed
|
||||
}
|
||||
|
||||
|
||||
@@ -367,11 +378,14 @@ class ApiSearchView(ApiView):
|
||||
)[:25]
|
||||
else:
|
||||
ops = qs.filter(
|
||||
Q(secret__istartswith=query) | Q(attendee_name__icontains=query) | Q(order__code__istartswith=query)
|
||||
Q(secret__istartswith=query)
|
||||
| Q(attendee_name__icontains=query)
|
||||
| Q(addon_to__attendee_name__icontains=query)
|
||||
| Q(order__code__istartswith=query)
|
||||
| Q(order__invoice_address__name__icontains=query)
|
||||
)[:25]
|
||||
|
||||
response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in ops]
|
||||
response['results'] = [serialize_op(op, bool(op.last_checked_in), self.config.list) for op in ops]
|
||||
else:
|
||||
response['results'] = []
|
||||
|
||||
@@ -393,7 +407,8 @@ class ApiDownloadView(ApiView):
|
||||
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event=self.event,
|
||||
order__status=Order.STATUS_PAID,
|
||||
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if self.config.list.include_pending else
|
||||
[]),
|
||||
subevent=self.config.list.subevent
|
||||
).annotate(
|
||||
last_checked_in=Subquery(cqs)
|
||||
@@ -405,7 +420,7 @@ class ApiDownloadView(ApiView):
|
||||
if not self.config.all_items:
|
||||
qs = qs.filter(item__in=self.config.items.all())
|
||||
|
||||
response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in qs]
|
||||
response['results'] = [serialize_op(op, bool(op.last_checked_in), self.config.list) for op in qs]
|
||||
|
||||
questions = self.event.questions.filter(ask_during_checkin=True).prefetch_related('items', 'options')
|
||||
response['questions'] = [serialize_question(q, items=True) for q in questions]
|
||||
@@ -417,11 +432,15 @@ class ApiStatusView(ApiView):
|
||||
|
||||
cqs = Checkin.objects.filter(
|
||||
position__order__event=self.event, position__subevent=self.subevent,
|
||||
position__order__status=Order.STATUS_PAID,
|
||||
position__order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if
|
||||
self.config.list.include_pending else []),
|
||||
list=self.config.list
|
||||
)
|
||||
pqs = OrderPosition.objects.filter(
|
||||
order__event=self.event, order__status=Order.STATUS_PAID, subevent=self.subevent,
|
||||
order__event=self.event,
|
||||
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if self.config.list.include_pending else
|
||||
[]),
|
||||
subevent=self.subevent,
|
||||
)
|
||||
if not self.config.list.all_products:
|
||||
pqs = pqs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True))
|
||||
|
||||
@@ -7,8 +7,9 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.db.models import Sum
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.timezone import now
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import pgettext, pgettext_lazy, ugettext as _
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
@@ -94,10 +95,11 @@ class ReportlabExportMixin:
|
||||
def page_footer(self, canvas, doc):
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
tz = get_current_timezone()
|
||||
canvas.setFont('OpenSans', 8)
|
||||
canvas.drawString(15 * mm, 10 * mm, _("Page %d") % (doc.page,))
|
||||
canvas.drawRightString(self.pagesize[0] - 15 * mm, 10 * mm,
|
||||
_("Created: %s") % now().strftime("%d.%m.%Y %H:%M:%S"))
|
||||
_("Created: %s") % now().astimezone(tz).strftime("%d.%m.%Y %H:%M:%S"))
|
||||
|
||||
def page_header(self, canvas, doc):
|
||||
from reportlab.lib.units import mm
|
||||
@@ -193,49 +195,50 @@ class OverviewReport(Report):
|
||||
]
|
||||
|
||||
items_by_category, total = order_overview(self.event, subevent=self.form_data.get('subevent'))
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
|
||||
for tup in items_by_category:
|
||||
if tup[0]:
|
||||
tstyledata.append(('FONTNAME', (0, len(tdata)), (-1, len(tdata)), 'OpenSansBd'))
|
||||
tdata.append([
|
||||
tup[0].name,
|
||||
str(tup[0].num_canceled[0]), localize(tup[0].num_canceled[1]),
|
||||
str(tup[0].num_refunded[0]), localize(tup[0].num_refunded[1]),
|
||||
str(tup[0].num_expired[0]), localize(tup[0].num_expired[1]),
|
||||
str(tup[0].num_pending[0]), localize(tup[0].num_pending[1]),
|
||||
str(tup[0].num_paid[0]), localize(tup[0].num_paid[1]),
|
||||
str(tup[0].num_total[0]), localize(tup[0].num_total[1]),
|
||||
str(tup[0].num_canceled[0]), floatformat(tup[0].num_canceled[1], places),
|
||||
str(tup[0].num_refunded[0]), floatformat(tup[0].num_refunded[1], places),
|
||||
str(tup[0].num_expired[0]), floatformat(tup[0].num_expired[1], places),
|
||||
str(tup[0].num_pending[0]), floatformat(tup[0].num_pending[1], places),
|
||||
str(tup[0].num_paid[0]), floatformat(tup[0].num_paid[1], places),
|
||||
str(tup[0].num_total[0]), floatformat(tup[0].num_total[1], places),
|
||||
])
|
||||
for item in tup[1]:
|
||||
tdata.append([
|
||||
" " + str(item.name),
|
||||
str(item.num_canceled[0]), localize(item.num_canceled[1]),
|
||||
str(item.num_refunded[0]), localize(item.num_refunded[1]),
|
||||
str(item.num_expired[0]), localize(item.num_expired[1]),
|
||||
str(item.num_pending[0]), localize(item.num_pending[1]),
|
||||
str(item.num_paid[0]), localize(item.num_paid[1]),
|
||||
str(item.num_total[0]), localize(item.num_total[1]),
|
||||
str(item.num_canceled[0]), floatformat(item.num_canceled[1], places),
|
||||
str(item.num_refunded[0]), floatformat(item.num_refunded[1], places),
|
||||
str(item.num_expired[0]), floatformat(item.num_expired[1], places),
|
||||
str(item.num_pending[0]), floatformat(item.num_pending[1], places),
|
||||
str(item.num_paid[0]), floatformat(item.num_paid[1], places),
|
||||
str(item.num_total[0]), floatformat(item.num_total[1], places),
|
||||
])
|
||||
if item.has_variations:
|
||||
for var in item.all_variations:
|
||||
tdata.append([
|
||||
" " + str(var),
|
||||
str(var.num_canceled[0]), localize(var.num_canceled[1]),
|
||||
str(var.num_refunded[0]), localize(var.num_refunded[1]),
|
||||
str(var.num_expired[0]), localize(var.num_expired[1]),
|
||||
str(var.num_pending[0]), localize(var.num_pending[1]),
|
||||
str(var.num_paid[0]), localize(var.num_paid[1]),
|
||||
str(var.num_total[0]), localize(var.num_total[1]),
|
||||
str(var.num_canceled[0]), floatformat(var.num_canceled[1], places),
|
||||
str(var.num_refunded[0]), floatformat(var.num_refunded[1], places),
|
||||
str(var.num_expired[0]), floatformat(var.num_expired[1], places),
|
||||
str(var.num_pending[0]), floatformat(var.num_pending[1], places),
|
||||
str(var.num_paid[0]), floatformat(var.num_paid[1], places),
|
||||
str(var.num_total[0]), floatformat(var.num_total[1], places),
|
||||
])
|
||||
|
||||
tdata.append([
|
||||
_("Total"),
|
||||
str(total['num_canceled'][0]), localize(total['num_canceled'][1]),
|
||||
str(total['num_refunded'][0]), localize(total['num_refunded'][1]),
|
||||
str(total['num_expired'][0]), localize(total['num_expired'][1]),
|
||||
str(total['num_pending'][0]), localize(total['num_pending'][1]),
|
||||
str(total['num_paid'][0]), localize(total['num_paid'][1]),
|
||||
str(total['num_total'][0]), localize(total['num_total'][1]),
|
||||
str(total['num_canceled'][0]), floatformat(total['num_canceled'][1], places),
|
||||
str(total['num_refunded'][0]), floatformat(total['num_refunded'][1], places),
|
||||
str(total['num_expired'][0]), floatformat(total['num_expired'][1], places),
|
||||
str(total['num_pending'][0]), floatformat(total['num_pending'][1], places),
|
||||
str(total['num_paid'][0]), floatformat(total['num_paid'][1], places),
|
||||
str(total['num_total'][0]), floatformat(total['num_total'][1], places),
|
||||
])
|
||||
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=3)
|
||||
|
||||
@@ -60,7 +60,7 @@ class SenderView(EventPermissionRequiredMixin, FormView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
qs = Order.objects.filter(event=self.request.event)
|
||||
qs = Order.objects.filter(event=self.request.event, email__isnull=False)
|
||||
statusq = Q(status__in=form.cleaned_data['sendto'])
|
||||
if 'overdue' in form.cleaned_data['sendto']:
|
||||
statusq |= Q(status=Order.STATUS_PENDING, expires__lt=now())
|
||||
|
||||
@@ -5,6 +5,7 @@ from collections import OrderedDict
|
||||
|
||||
import stripe
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.template.loader import get_template
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
@@ -165,6 +166,10 @@ class StripeMethod(BasePaymentProvider):
|
||||
def order_prepare(self, request, order):
|
||||
return self.checkout_prepare(request, None)
|
||||
|
||||
def _get_amount(self, order):
|
||||
places = settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
return int(order.total * 10 ** places)
|
||||
|
||||
def _init_api(self):
|
||||
stripe.api_version = '2017-06-05'
|
||||
stripe.api_key = self.settings.get('secret_key')
|
||||
@@ -180,7 +185,7 @@ class StripeMethod(BasePaymentProvider):
|
||||
def _charge_source(self, request, source, order):
|
||||
try:
|
||||
charge = stripe.Charge.create(
|
||||
amount=int(order.total * 100),
|
||||
amount=self._get_amount(order),
|
||||
currency=self.event.currency.lower(),
|
||||
source=source,
|
||||
metadata={
|
||||
@@ -269,7 +274,7 @@ class StripeMethod(BasePaymentProvider):
|
||||
if order.payment_info:
|
||||
payment_info = json.loads(order.payment_info)
|
||||
if 'amount' in payment_info:
|
||||
payment_info['amount'] /= 100
|
||||
payment_info['amount'] /= 10 ** settings.CURRENCY_PLACES.get(self.event.currency, 2)
|
||||
else:
|
||||
payment_info = None
|
||||
template = get_template('pretixplugins/stripe/control.html')
|
||||
@@ -411,7 +416,7 @@ class StripeCC(StripeMethod):
|
||||
request.session['payment_stripe_order_secret'] = order.secret
|
||||
source = stripe.Source.create(
|
||||
type='three_d_secure',
|
||||
amount=int(order.total * 100),
|
||||
amount=self._get_amount(order),
|
||||
currency=self.event.currency.lower(),
|
||||
three_d_secure={
|
||||
'card': src.id
|
||||
@@ -479,7 +484,7 @@ class StripeGiropay(StripeMethod):
|
||||
try:
|
||||
source = stripe.Source.create(
|
||||
type='giropay',
|
||||
amount=int(order.total * 100),
|
||||
amount=self._get_amount(order),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
@@ -538,7 +543,7 @@ class StripeIdeal(StripeMethod):
|
||||
def _create_source(self, request, order):
|
||||
source = stripe.Source.create(
|
||||
type='ideal',
|
||||
amount=int(order.total * 100),
|
||||
amount=self._get_amount(order),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
@@ -585,7 +590,7 @@ class StripeAlipay(StripeMethod):
|
||||
def _create_source(self, request, order):
|
||||
source = stripe.Source.create(
|
||||
type='alipay',
|
||||
amount=int(order.total * 100),
|
||||
amount=self._get_amount(order),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
@@ -634,7 +639,7 @@ class StripeBancontact(StripeMethod):
|
||||
try:
|
||||
source = stripe.Source.create(
|
||||
type='bancontact',
|
||||
amount=int(order.total * 100),
|
||||
amount=self._get_amount(order),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
@@ -706,7 +711,7 @@ class StripeSofort(StripeMethod):
|
||||
def _create_source(self, request, order):
|
||||
source = stripe.Source.create(
|
||||
type='sofort',
|
||||
amount=int(order.total * 100),
|
||||
amount=self._get_amount(order),
|
||||
currency=self.event.currency.lower(),
|
||||
metadata={
|
||||
'order': str(order.id),
|
||||
|
||||
@@ -29,4 +29,4 @@ class AllTicketsPDF(BaseExporter):
|
||||
|
||||
p.save()
|
||||
outbuffer = o._render_with_background(buffer)
|
||||
return 'tickets.pdf', 'application/pdf', outbuffer.read()
|
||||
return '{}_tickets.pdf'.format(self.event.slug), 'application/pdf', outbuffer.read()
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import copy
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from io import BytesIO
|
||||
|
||||
import bleach
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.formats import date_format, localize
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pytz import timezone
|
||||
from reportlab.graphics import renderPDF
|
||||
@@ -27,6 +29,7 @@ from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.ticketoutput import BaseTicketOutput
|
||||
from pretix.plugins.ticketoutputpdf.signals import (
|
||||
get_fonts, layout_text_variables,
|
||||
@@ -79,7 +82,7 @@ DEFAULT_VARIABLES = OrderedDict((
|
||||
("price", {
|
||||
"label": _("Price"),
|
||||
"editor_sample": _("123.45 EUR"),
|
||||
"evaluate": lambda op, order, event: '{} {}'.format(event.currency, localize(op.price))
|
||||
"evaluate": lambda op, order, event: money_filter(op.price, event.currency)
|
||||
}),
|
||||
("attendee_name", {
|
||||
"label": _("Attendee name"),
|
||||
@@ -242,8 +245,14 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
|
||||
alignment=align_map[o['align']]
|
||||
)
|
||||
|
||||
p = Paragraph(self._get_text_content(op, order, o) or "", style=style)
|
||||
text = re.sub(
|
||||
"<br[^>]*>", "<br/>",
|
||||
bleach.clean(
|
||||
self._get_text_content(op, order, o) or "",
|
||||
tags=["br"], attributes={}, styles=[], strip=True
|
||||
)
|
||||
)
|
||||
p = Paragraph(text, style=style)
|
||||
p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
|
||||
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
|
||||
ad = getAscentDescent(font, float(o['fontsize']))
|
||||
|
||||
@@ -312,11 +312,12 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
initial.update(self.cart_session.get('contact_form_data', {}))
|
||||
return ContactForm(data=self.request.POST if self.request.method == "POST" else None,
|
||||
event=self.request.event,
|
||||
request=self.request,
|
||||
initial=initial)
|
||||
|
||||
@cached_property
|
||||
def eu_reverse_charge_relevant(self):
|
||||
return any([p.item.tax_rule and p.item.tax_rule.eu_reverse_charge
|
||||
return any([p.item.tax_rule and (p.item.tax_rule.eu_reverse_charge or p.item.tax_rule.custom_rules)
|
||||
for p in self.positions])
|
||||
|
||||
@cached_property
|
||||
@@ -337,10 +338,10 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
_("We had difficulties processing your input. Please review the errors below."))
|
||||
return self.render()
|
||||
self.cart_session['email'] = self.contact_form.cleaned_data['email']
|
||||
self.cart_session['contact_form_data'] = self.contact_form.cleaned_data
|
||||
if request.event.settings.invoice_address_asked:
|
||||
addr = self.invoice_form.save()
|
||||
self.cart_session['invoice_address'] = addr.pk
|
||||
self.cart_session['contact_form_data'] = self.contact_form.cleaned_data
|
||||
|
||||
update_tax_rates(
|
||||
event=request.event,
|
||||
@@ -407,6 +408,7 @@ class QuestionsStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
ctx['invoice_form'] = self.invoice_form
|
||||
ctx['reverse_charge_relevant'] = self.eu_reverse_charge_relevant
|
||||
ctx['cart'] = self.get_cart()
|
||||
ctx['cart_session'] = self.cart_session
|
||||
return ctx
|
||||
|
||||
|
||||
@@ -486,9 +488,13 @@ class PaymentStep(QuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
|
||||
def is_applicable(self, request):
|
||||
self.request = request
|
||||
if self._total_order_value == 0:
|
||||
self.cart_session['payment'] = 'free'
|
||||
return False
|
||||
|
||||
for p in self.request.event.get_payment_providers().values():
|
||||
if p.is_implicit:
|
||||
if p.is_allowed(request):
|
||||
self.cart_session['payment'] = p.identifier
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -516,11 +522,14 @@ class ConfirmStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
ctx['confirm_messages'] = self.confirm_messages
|
||||
ctx['cart_session'] = self.cart_session
|
||||
|
||||
ctx['contact_info'] = []
|
||||
responses = contact_form_fields.send(self.event)
|
||||
ctx['contact_info'] = [
|
||||
(_('E-mail'), self.cart_session.get('contact_form_data', {}).get('email')),
|
||||
]
|
||||
responses = contact_form_fields.send(self.event, request=self.request)
|
||||
for r, response in sorted(responses, key=lambda r: str(r[0])):
|
||||
for key, value in response.items():
|
||||
v = self.cart_session.get('contact_form_data', {}).get(key)
|
||||
v = value.bound_data(v, initial='')
|
||||
if v is True:
|
||||
v = _('Yes')
|
||||
elif v is False:
|
||||
|
||||
0
src/pretix/presale/forms/__init__.py
Normal file
0
src/pretix/presale/forms/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user