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