Compare commits

...

68 Commits

Author SHA1 Message Date
Raphael Michel
77ffe55453 Bump version to 1.13.1 2018-03-07 10:37:23 +01:00
Raphael Michel
ab865e716f Allow admin to create invoice if invoice setting is set to "all orders" 2018-03-07 10:37:13 +01:00
Raphael Michel
0bf1832b23 Allow customer to manually generate invoices if order is older than invoice setting 2018-03-07 10:36:00 +01:00
Raphael Michel
650adb9235 pretixdroid: Online search should include name of parent position 2018-03-07 10:36:00 +01:00
Raphael Michel
e2d55fed0d Fix issue with fees without tax rules 2018-03-07 10:36:00 +01:00
Raphael Michel
aef751dbee Contact form data was only saved to session if invoice addresses where active 2018-03-07 10:36:00 +01:00
Raphael Michel
cd084fe8d1 Show "continue" instead of "checkout" also if order is free 2018-03-07 10:36:00 +01:00
Raphael Michel
c68b6116a2 Bump version to 1.13.0 2018-03-03 21:38:07 +01:00
Raphael Michel
f0db879c9c Update docs and German translation 2018-03-03 21:16:17 +01:00
Felix Rindt
07d8a3d765 Fix #774 -- Make question options sortable (#786)
* add position field

* add question option sorting logic
* add meta class to question option for sorting
* regenerate migration
* add template content and view mechanics

* Rename migration after rebase & update dependency
2018-03-03 20:36:30 +01:00
Raphael Michel
e35e264d81 Improve voucher redemption filter (#792) 2018-03-03 11:58:59 +01:00
Raphael Michel
d537e6a869 Order confirmation: Add e-mail to contact information box 2018-03-03 11:56:33 +01:00
Leonardo
d4dd1861a9 Fix #740 -- Date picker: Fix line height for decade span (#761)
* Fix line height for decade span

* Move to own file
2018-03-03 11:31:23 +01:00
Mohit Jindal
3019a31fbb Fix #735 -- Display of event series on public organizer page (#753) 2018-03-03 11:24:07 +01:00
Raphael Michel
303b9912ff Add „button“ operation mode of the widget (#778) 2018-03-03 11:20:41 +01:00
Raphael Michel
0259b2e5b9 Update paypal documentation 2018-03-02 22:55:37 +01:00
Raphael Michel
5c7e8029f4 Fix incorrect test case 2018-03-02 22:05:56 +01:00
Raphael Michel
08e3fd3141 Fix spelling 2018-03-02 21:54:36 +01:00
Raphael Michel
30123fd6ff Add currency property to subevent 2018-03-02 21:54:08 +01:00
Raphael Michel
3955299983 Catch VAT WebServiceError 2018-03-01 09:21:21 +01:00
Raphael Michel
b5d0df3ca7 Fix determination of VAT ID validation 2018-03-01 09:19:04 +01:00
Raphael Michel
22c65da9d1 Fix invalid use of money_filter 2018-03-01 09:17:59 +01:00
Raphael Michel
578c1ecfaf Add support for custom taxation rules 2018-02-28 23:03:25 +01:00
Raphael Michel
d8d00a7e26 Add total argument to fee calculation signals 2018-02-28 21:03:38 +01:00
Raphael Michel
37f0f7a138 Add service fees as a first-level fee type 2018-02-27 22:39:07 +01:00
Raphael Michel
f61e9367ec Update German translation 2018-02-26 10:51:44 +01:00
Raphael Michel
3c3e59e932 Refs #99 -- Improve support for currencies with less than 2 decimal places (#783)
* Refs #99 -- Fix stripe support for zero-decimal currencies

* Add new money formatting method

* Force decimal places in many places

* Locale-aware currency rendering

* Fix currencies in more places

* More currency fixes
2018-02-26 10:46:07 +01:00
Raphael Michel
29e22a0c6c Fix check-in of unpaid orders in web check-in list 2018-02-26 10:42:58 +01:00
Raphael Michel
0d1f424425 Improve performance of voucher bulk creation 2018-02-26 10:42:58 +01:00
Tim Freund
1c01e23867 Name presale index + unit test for URL names (#784)
* Name the default URL

If metrics collection is enabled, the index page of the site will fail
to load: without a name, the metrics middleware throws a TypeError.

* Test for names on all URLs

This test passes if all URLs have names. Without names, URLs will cause
the optional metrics middleware to throw a TypeError.
2018-02-26 10:17:42 +01:00
Felix Rindt
f763a8694b Fix #779: add form field for unpaid option of checkin lists in subevent detail view (#781)
* add form field for unpaid option of checkin lists in subevent detail view

* change order of include_pending field

* also change the order in new check in lists
2018-02-26 10:17:28 +01:00
Raphael Michel
675b853b29 Remove organizer property from ICalendar files as we used it not as it is intended to be used. 2018-02-23 10:51:32 +01:00
Raphael Michel
2434bf14d5 Add checkin_attetion field to Order model 2018-02-22 13:25:26 +01:00
Felix Rindt
70fbbfe2a0 Refs #757: show voucher input for subevents only if subevent is selected (#777)
* show voucher input for subevents only if subevent is selected

* move logic to python
2018-02-22 09:44:53 +01:00
Raphael Michel
e096898a05 Update German translation 2018-02-21 16:17:06 +01:00
Raphael Michel
3fbccf3f64 Allow check-in lists to include unpaid orders 2018-02-21 16:17:06 +01:00
Raphael Michel
36585395f1 Voucher list: add more filters 2018-02-21 16:17:06 +01:00
Felix Rindt
e4b0a1613f Refs #754 -- check item tax_rule is not none (#776) 2018-02-21 12:51:50 +01:00
Raphael Michel
1192e474c5 Prevent duplicate All/None links 2018-02-20 10:20:24 +01:00
Raphael Michel
e48ea99e48 Fix datetime in check-in list on MySQL 2018-02-20 10:19:55 +01:00
Raphael Michel
072f2a0ee9 Pin sessions to the user agent in use 2018-02-19 13:02:55 +01:00
Tim Freund
aecb536a34 Use config.getboolean to get metrics enabled value (#770)
Given the following configuration:

[metrics]
enabled=False

Using config.get results in a METRICS_ENABLED value that always
evaluates to True. This PR switches to config.getboolean so that metrics
can be disabled without deleting the configuration values.
2018-02-18 17:40:13 +01:00
Tim Freund
a68686cb06 Docs: Fix link to the Celery configuration documentation (#771) 2018-02-18 17:39:51 +01:00
Tim Freund
ba8cf3e01e Replace PREFIX_CONFIG_FILE with PRETIX_CONFIG_FILE (#769)
The code looks for PRETIX_CONFIG_FILE in src/pretix/settings.py.
This change updates the documentation to match.
2018-02-18 17:39:34 +01:00
Raphael Michel
b0c5189c4b Fix timezone for footer of printed exports 2018-02-14 11:50:24 +01:00
Raphael Michel
d44eb67dec Allow http: forms during testing 2018-02-14 11:50:10 +01:00
Raphael Michel
58d36b08e2 Pin Sphinx version 2018-02-14 11:49:50 +01:00
Raphael Michel
98906731e3 Move plugin list to website 2018-02-14 11:49:44 +01:00
Raphael Michel
035a4b0928 Add next parameter to logout view 2018-02-14 11:49:16 +01:00
Raphael Michel
85fbe666ea Order modification page: Make cancel button more useful 2018-02-12 12:38:30 +01:00
Tobias Kunze
741d0bc686 Put event slugs in export filenames (#768) 2018-02-12 12:30:13 +01:00
Raphael Michel
ded539ce7a Ignore event end date for subevents 2018-02-07 13:51:22 +01:00
Raphael Michel
c53fd25d1c Use a consistant CSS compression method 2018-02-05 13:48:47 +01:00
Raphael Michel
da32621c55 Add "is_implicit" attribute to payment providers 2018-02-04 23:14:18 +01:00
Raphael Michel
4ccf33af03 Add support for orders without email addresses 2018-02-04 22:42:41 +01:00
Raphael Michel
a5af7a70f3 Add support for iframeResizer 2018-02-04 22:42:04 +01:00
Raphael Michel
16ab0d29d6 Add request argument to contact_form_fields signal 2018-02-04 22:15:58 +01:00
Raphael Michel
05ad9022c0 Always use full width when used in an iframe 2018-02-04 22:02:54 +01:00
Raphael Michel
fef211b220 Change typeahead.css and morris.css to scss files 2018-02-04 21:06:44 +01:00
Raphael Michel
6aee1ee41f Stip HTML from text in PDFs except for <br>, make <br> not break things 2018-02-04 19:45:00 +01:00
Raphael Michel
bab7f9b1f3 Notification view: use select2 event selection 2018-02-04 19:09:22 +01:00
Raphael Michel
340e7afd06 Fix bug that lead to notifications being sent for all events 2018-02-04 18:53:56 +01:00
Raphael Michel
cb83c9cff2 Add a short system check before publishing packages 2018-02-04 18:33:50 +01:00
Raphael Michel
911a8fed06 Fix waiting list test 2018-02-04 18:28:29 +01:00
Raphael Michel
eb8b43fe36 Add missing __init__.py file 2018-02-04 18:27:45 +01:00
Raphael Michel
2a15dc57d8 Waiting list: Do not send out for disabled events 2018-02-04 14:24:53 +01:00
Raphael Michel
67678e35bb Disable shop and waiting list after end of event 2018-02-04 14:14:49 +01:00
Raphael Michel
2f00db8081 Bump version to 1.13.0.dev0 2018-02-03 17:00:40 +01:00
144 changed files with 7314 additions and 3133 deletions

1
.gitattributes vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -102,6 +102,8 @@ The provider class
.. automethod:: order_control_refund_perform
.. automethod:: is_implicit
Additional views
----------------

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
-r ../src/requirements.txt
sphinx
sphinx==1.6.*
sphinx-rtd-theme
sphinxcontrib-httpdomain
sphinxcontrib-images

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
__version__ = "1.12.0"
__version__ = "1.13.1"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -36,7 +36,8 @@ class CheckinListForm(forms.ModelForm):
'name',
'all_products',
'limit_products',
'subevent'
'subevent',
'include_pending'
]
widgets = {
'limit_products': forms.CheckboxSelectMultiple(attrs={

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 }}&nbsp;&nbsp;
{{ order.email|default_if_none:"" }}&nbsp;&nbsp;
<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" %}

View File

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

View File

@@ -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 }}&amp;status=c&amp;provider={{ item.provider }}">
{{ item.num_canceled|togglesum }}
{{ item.num_canceled|togglesum:request.event.currency }}
</a>
</td>
<td>
<a href="{{ listurl }}?item={{ item.id }}&amp;status=r&amp;provider={{ item.provider }}">
{{ item.num_refunded|togglesum }}
{{ item.num_refunded|togglesum:request.event.currency }}
</a>
</td>
<td>
<a href="{{ listurl }}?item={{ item.id }}&amp;status=e&amp;provider={{ item.provider }}">
{{ item.num_expired|togglesum }}
{{ item.num_expired|togglesum:request.event.currency }}
</a>
</td>
<td>
<a href="{{ listurl }}?item={{ item.id }}&amp;status=n&amp;provider={{ item.provider }}">
{{ item.num_pending|togglesum }}
{{ item.num_pending|togglesum:request.event.currency }}
</a>
</td>
<td>
<a href="{{ listurl }}?item={{ item.id }}&amp;status=p&amp;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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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