Compare commits

...

93 Commits

Author SHA1 Message Date
Raphael Michel
f4c8446cd5 Bump to 4.4.1 2022-01-26 13:43:14 +01:00
Raphael Michel
84fdcacb17 [SECURITY] Make redirect view dependent on referer 2022-01-26 13:42:05 +01:00
Raphael Michel
2c7c04ae88 [SECURITY] Fix (non-exploitable) XSS issue 2022-01-26 13:42:05 +01:00
Raphael Michel
5e3f9178f5 Bump version to 4.4.0 2021-10-29 15:37:30 +02:00
Raphael Michel
03ea47715d Fix check_order_transactions on SQLite 2021-10-29 15:37:22 +02:00
Raphael Michel
e8936551c0 Extend spellcheck word list 2021-10-29 13:58:29 +02:00
Raphael Michel
ea0f6dfc54 Update po files
[CI skip]

Signed-off-by: Raphael Michel <michel@rami.io>
2021-10-29 12:10:08 +02:00
Raphael Michel
abeddd360e Invoices: Change expected behaviour for switches in numbering scheme 2021-10-29 12:09:09 +02:00
Maarten van den Berg
c209d195bf Translated on translate.pretix.eu (Dutch)
Currently translated at 100.0% (172 of 172 strings)

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

powered by weblate
2021-10-29 10:24:22 +02:00
Maarten van den Berg
35c46d320c Translated on translate.pretix.eu (Dutch)
Currently translated at 99.9% (4470 of 4474 strings)

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

powered by weblate
2021-10-29 10:24:22 +02:00
Raphael Michel
30621568ab Added translation on translate.pretix.eu (Galician) 2021-10-29 10:24:22 +02:00
Raphael Michel
403c4f4499 Add Galician as an incubating language 2021-10-29 10:23:57 +02:00
Raphael Michel
884bba0088 Fix transaction creation during split order creation 2021-10-29 10:21:37 +02:00
Raphael Michel
2b52edd5b7 Remove wrong optimization 2021-10-28 11:12:16 +02:00
Richard Schreiber
a4aed96784 Fix: add support for rtl-languages to checkout-step-bars 2021-10-27 16:16:04 +02:00
Raphael Michel
4bdfd56264 E-mail layout with logo: Make image display:block for outlook 2021-10-27 11:31:33 +02:00
Raphael Michel
31f0b07325 FIx typo causing test failure 2021-10-27 11:09:00 +02:00
pretix translation bot
3f08f3a7f4 Translations update from Weblate (#2266)
Co-authored-by: Tony Pavlik <kontakt@playton.cz>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2021-10-27 09:22:02 +02:00
Raphael Michel
93263e7567 money template filter: coerce None to 0.00 2021-10-26 18:07:37 +02:00
Raphael Michel
69cf62d2ca Fix missing or wrong create_transactions calls 2021-10-26 18:07:23 +02:00
Raphael Michel
bb353e5fde Improve detection of missing transactions 2021-10-26 18:06:49 +02:00
Raphael Michel
2dceff1218 Fix transaction creation issues and improve debugging 2021-10-26 11:33:44 +02:00
Raphael Michel
5ea8a8ef82 Ask and validate VAT IDs for Switzerland (#2259)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-10-26 11:20:45 +02:00
pretix translation bot
03a7a3303c Translations update from Weblate (#2264)
Co-authored-by: Tony Pavlik <kontakt@playton.cz>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Jacek Wielemborek <github@d33.pl>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
2021-10-26 11:19:45 +02:00
Raphael Michel
2beb0b20ca Check-in API: Work around libpretixsync issue with space encoding 2021-10-26 10:46:28 +02:00
Richard Schreiber
24eea02e0d API: sort ordered items’ answers by questions’ position (#2182) 2021-10-26 09:42:01 +02:00
Raphael Michel
15ab9c72d3 Invoice renderer: Reduce a few spacings 2021-10-22 13:10:48 +02:00
Raphael Michel
c957d77fe0 Fix linter issues 2021-10-22 12:58:45 +02:00
Raphael Michel
7697018ca4 Order JSON export: Add a lot more fields 2021-10-22 12:43:41 +02:00
Raphael Michel
3980a7b2a7 Docs: Fix missing files 2021-10-22 11:06:23 +02:00
pretix translation bot
035bb56386 Translations update from Weblate (#2254)
Co-authored-by: Tony Pavlik <kontakt@playton.cz>
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Jacek Wielemborek <github@d33.pl>
Co-authored-by: Maarten van den Berg <maartenberg1@gmail.com>
2021-10-22 11:04:42 +02:00
Raphael Michel
837b03fff3 Add ugprade note to docs 2021-10-22 11:01:35 +02:00
Raphael Michel
d3dec72831 Add missing import 2021-10-22 10:26:10 +02:00
Raphael Michel
3d78f68d94 Docs: Add page on errors 2021-10-22 10:25:58 +02:00
Raphael Michel
faa43d4df8 Remove duplicate form field 2021-10-21 13:25:52 +02:00
Raphael Michel
78917afa1a Event settings API: Expose mail_days_order_expire_warning 2021-10-19 17:12:13 +02:00
Raphael Michel
4b53d39e3e Add debug command check_order_transactions 2021-10-19 17:10:08 +02:00
Raphael Michel
02db07cd25 Work around potential caching issue 2021-10-19 17:04:28 +02:00
Raphael Michel
19fb6c8c34 create_order_transactions: Make suitable for large datasets 2021-10-19 15:25:34 +02:00
Raphael Michel
0c25b2df92 Docs: Fix typo in index name 2021-10-19 15:25:15 +02:00
Raphael Michel
6a543e4557 Fix missing log message 2021-10-18 18:50:53 +02:00
Raphael Michel
846527546a Improve visual transaction table 2021-10-18 18:35:02 +02:00
Raphael Michel
c8cdb2b311 Log silent DirtyTransactionsForOrderException to sentry 2021-10-18 17:57:36 +02:00
Raphael Michel
96ff3d532d Fix logic error 2021-10-18 17:55:32 +02:00
Raphael Michel
8ebba9de86 Data model for transactional history (#2147) 2021-10-18 17:28:58 +02:00
Raphael Michel
c4e71011ee Update English wordlist 2021-10-18 13:24:38 +02:00
Raphael Michel
e71ad4bfba CSS: Always clear floats before drawing footer 2021-10-18 10:37:23 +02:00
Raphael Michel
05a5a69128 Lightbox: Remove .min.js and make dependency on gettext optional 2021-10-18 09:23:12 +02:00
Raphael Michel
bb83cd2f39 Fix duplicate margin 2021-10-17 19:16:19 +02:00
Raphael Michel
df26171ff1 Fix/Improve responsiveness of calendar pages 2021-10-17 18:55:01 +02:00
Raphael Michel
da937dc4e3 [a11y] Small fixes and improvements 2021-10-17 18:35:55 +02:00
Raphael Michel
bb9508ad96 Fix typo 2021-10-17 17:38:34 +02:00
Raphael Michel
41fed7d6a2 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 98.8% (170 of 172 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
f441e9984d Translated on translate.pretix.eu (German)
Currently translated at 98.8% (170 of 172 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
05c6155f37 Translated on translate.pretix.eu (German (informal) (de_Informal))
Currently translated at 100.0% (4474 of 4474 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
3c096325bd Translated on translate.pretix.eu (German)
Currently translated at 100.0% (4474 of 4474 strings)

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

powered by weblate
2021-10-17 17:37:17 +02:00
Raphael Michel
d06a352df5 Update wordlist 2021-10-17 17:36:58 +02:00
Raphael Michel
ba7b1bb89e Update po files
[CI skip]

Signed-off-by: Raphael Michel <mail@raphaelmichel.de>
2021-10-17 16:57:30 +02:00
Richard Schreiber
3dcfa57b70 A11y improvements (#2081)
Co-authored-by: Raphael Michel <michel@rami.io>
Co-authored-by: Raphael Michel <mail@raphaelmichel.de>
2021-10-17 16:56:16 +02:00
Raphael Michel
cc13ca1c3f Fix #2165 -- Idempotency key errors from Stripe 2021-10-15 12:01:58 +02:00
Raphael Michel
aac67ebf83 Refs #2165 -- Lock payment object while processing Stripe response 2021-10-15 11:57:40 +02:00
Raphael Michel
b51e1cfc6f Fix #2241 -- Display timezone for sale start 2021-10-15 11:46:45 +02:00
Raphael Michel
f0508cdcc3 Fix #2228 -- Date filter behavior in order data export 2021-10-15 11:46:45 +02:00
Raphael Michel
9ed2dc7b46 Add exporter for gift card transactions 2021-10-15 11:46:45 +02:00
Raphael Michel
0e568a3fca Translated on translate.pretix.eu (Spanish)
Currently translated at 67.8% (2995 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
ityd
7f3606ee81 Translated on translate.pretix.eu (Spanish)
Currently translated at 67.8% (2996 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
DJG Bayern
b22d43860a Translated on translate.pretix.eu (Japanese)
Currently translated at 2.9% (5 of 171 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
DJG Bayern
0f9b339f01 Translated on translate.pretix.eu (Japanese)
Currently translated at 0.1% (4 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
DJG Bayern
cd1e9c1740 Translated on translate.pretix.eu (Japanese)
Currently translated at 0.1% (2 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
Raphael Michel
aec1ce53fc Added translation on translate.pretix.eu (Japanese) 2021-10-15 11:18:45 +02:00
Raphael Michel
aae129be6a Added translation on translate.pretix.eu (Japanese) 2021-10-15 11:18:45 +02:00
Tony Pavlik
b906fe0fc3 Translated on translate.pretix.eu (Czech)
Currently translated at 8.8% (390 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
Adri
f0f1537e9c Translated on translate.pretix.eu (French)
Currently translated at 50.7% (2238 of 4413 strings)

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

powered by weblate
2021-10-15 11:18:45 +02:00
Raphael Michel
7b7e77d497 Subevent editor: Fix Quota.ignore_for_event_availability not being copied 2021-10-15 11:12:36 +02:00
Raphael Michel
9ac705cd88 Web check-in: Show subevent with check result 2021-10-14 18:48:19 +02:00
Richard Schreiber
01d9574ddf Fix #2244 -- Show products without category first on product-list (#2249) 2021-10-13 09:33:43 +02:00
Richard Schreiber
8121167d5e Control: Add drag and drop to sort categories and products (#2242)
* add drag and drop to categories

* add drag and drop to products

* add light grey background to dragged element

* add missing th, add sr-only desc of columns

* group up/down/move elements

* improve visualizing drag-area by dimming others

* change up/down-links to buttons in form-post

* limit sorting to POST requests

Co-authored-by: Raphael Michel <michel@rami.io>
2021-10-12 14:46:56 +02:00
Raphael Michel
dde4e12ce1 Fix bug in 6cd32400a 2021-10-11 17:36:57 +02:00
Raphael Michel
6cd32400ae Mails: Add elaborate retry logic for MS Exchange 2021-10-11 12:41:26 +02:00
Raphael Michel
8fa71ccad4 Show remaining quota on voucher redemption page 2021-10-08 18:08:28 +02:00
Raphael Michel
0f47bff5cd Allow to hide products that require membership (#2240)
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2021-10-07 10:11:31 +02:00
Raphael Michel
f459f1f12d Fix logging error for automated emails 2021-10-07 10:08:30 +02:00
Richard Schreiber
65167cc290 Add new alert icons (#2226) 2021-10-06 12:31:08 +02:00
Raphael Michel
bc7300c393 Track if invoices have been sent via email (#2231) 2021-10-05 13:47:55 +02:00
Jaakko Rinta-Filppula
d8450202fe Translated on translate.pretix.eu (Finnish)
Currently translated at 50.2% (86 of 171 strings)

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

powered by weblate
2021-10-05 12:47:08 +02:00
Jaakko Rinta-Filppula
41d2bcc34f Translated on translate.pretix.eu (Finnish)
Currently translated at 19.3% (852 of 4413 strings)

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

powered by weblate
2021-10-05 12:47:08 +02:00
Fabian Rodriguez
0e1589013a Translated on translate.pretix.eu (French)
Currently translated at 63.7% (109 of 171 strings)

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

powered by weblate
2021-10-04 17:34:05 +02:00
cpoisnel
39f81617e1 Translated on translate.pretix.eu (French)
Currently translated at 63.7% (109 of 171 strings)

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

powered by weblate
2021-10-04 17:34:05 +02:00
cpoisnel
b394ef6de1 Translated on translate.pretix.eu (French)
Currently translated at 50.5% (2231 of 4413 strings)

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

powered by weblate
2021-10-04 17:34:05 +02:00
Raphael Michel
177906e2ac Custom order emails: Allow to attach tickets and invoices 2021-09-30 12:15:55 +02:00
Raphael Michel
59f6b20129 Add email placeholder {voucher_url_list} 2021-09-30 11:54:41 +02:00
Raphael Michel
51998e820d Orders API: Add item and variation filters 2021-09-30 11:48:23 +02:00
Raphael Michel
e803b56716 Bump to 4.4.0.dev0 2021-09-29 11:17:50 +02:00
219 changed files with 128070 additions and 62919 deletions

40
doc/admin/errors.rst Normal file
View File

@@ -0,0 +1,40 @@
.. _`admin-errors`:
Dealing with errors
===================
If you encounter an error in pretix, please follow the following steps to debug it:
* If the error message is shown on a **white page** and the last line of the error includes "nginx", the error is not with pretix
directly but with your nginx webserver. This might mean that pretix is not running, but it could also be something else.
Please first check your nginx error log. The default location is ``/var/log/nginx/error.log``.
* If it turns out pretix is not running, check the output of ``docker logs pretix`` for a docker installation and
``journalctl -u pretix-web.service`` for a manual installation.
* If the error message is an "**Internal Server Error**" in purple pretix design, please check pretix' log file which by default is at
``/var/pretix-data/logs/pretix.log`` if you installed with docker and ``/var/pretix/data/logs/pretix.log`` otherwise. If you don't
know how to interpret it, open a discussion on GitHub with the relevant parts of the log file.
* If the error message includes ``/usr/bin/env: node: No such file or directory``, you forgot to install ``node.js``
* If the error message includes ``OfflineGenerationError``, you might have forgot to run the ``rebuild`` step after a pretix update
or plugin installation.
* If the error message mentions your database server or redis server, make sure these are running and accessible.
* If pretix loads fine but certain actions (creating carts, orders, or exports, downloading tickets, sending emails) **take forever**,
``pretix-worker`` is not running. Check the output of ``docker logs pretix`` for a docker installation and
``journalctl -u pretix-worker.service`` for a manual installation.
* If the page loads but all **styles are missing**, you probably forgot to update your nginx configuration file after an upgrade of your
operating system's python version.
If you are unable to debug the issue any further, please open a **discussion** on GitHub in our `Q&A Forum`_. Do **not** open an issue
right away, since most things turn out not to be a bug in pretix but a mistake in your server configuration. Make sure to include
relevant log excerpts in your question.
If you're a pretix Enterprise customer, you can also reach out to support@pretix.eu with your issue right away.
.. _Q&A Forum: https://github.com/pretix/pretix/discussions/categories/q-a

View File

@@ -9,7 +9,9 @@ This documentation is for everyone who wants to install pretix on a server.
:maxdepth: 2
installation/index
updates
config
maintainance
scaling
errors
indexes

View File

@@ -50,7 +50,7 @@ Here is the currently recommended set of commands::
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_name
ON pretixbase_orderposition
USING gin (upper("attendee_name_cached") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_scret
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_secret
ON pretixbase_orderposition
USING gin (upper("secret") gin_trgm_ops);
CREATE INDEX CONCURRENTLY pretix_addidx_orderpos_email

View File

@@ -256,6 +256,8 @@ create an event and start selling tickets!
You should probably read :ref:`maintainance` next.
.. _`docker_updates`:
Updates
-------
@@ -271,6 +273,8 @@ Restarting the service can take a few seconds, especially if the update requires
Replace ``stable`` above with a specific version number like ``1.0`` or with ``latest`` for the development
version, if you want to.
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
.. _`docker_plugininstall`:
Install a plugin

View File

@@ -280,6 +280,8 @@ create an event and start selling tickets!
You should probably read :ref:`maintainance` next.
.. _`manual_updates`:
Updates
-------
@@ -294,6 +296,7 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
(venv)$ python -m pretix updatestyles
# systemctl restart pretix-web pretix-worker
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to.
.. _`manual_plugininstall`:

View File

@@ -9,6 +9,8 @@ If you host your own pretix instance, you also need to care about the availabili
of your service and the safety of your data yourself. This page gives you some
information that you might need to do so properly.
.. _`backups`:
Backups
-------

44
doc/admin/updates.rst Normal file
View File

@@ -0,0 +1,44 @@
.. _`update_notes`:
Update notes
============
pretix receives regular feature and bugfix updates and we highly encourage you to always update to
the latest version for maximum quality and security. Updates are announces on our `blog`_. There are
usually 10 feature updates in a year, so you can expect a new release almost every month.
Pure bugfix releases are only issued in case of very critical bugs or security vulnerabilities. In these
case, we'll publish bugfix releases for the last three stable release branches.
Compatibility to plugins and in very rare cases API clients may break. For in-depth details on the
API changes of every version, please refer to the release notes published on our blog.
Upgrade steps
-------------
For the actual upgrade, you can usually just follow the steps from the installation guide for :ref:`manual installations <manual_updates>`
or :ref:`docker installations <docker_updates>` respectively.
Generally, it is always strongly recommended to perform a :ref:`backup <backups>` first.
It is possible to skip versions during updates, although we recommend not skipping over major version numbers
(i.e. if you want to go from 2.4 to 4.4, first upgrade to 3.0, then upgrade to 4.0, then to 4.4).
In addition to these standard update steps, the following list issues steps that should be taken when you upgrade
to specific versions for pretix. If you're skipping versions, please read the instructions for every version in
between as well.
Upgrade to 4.4.0 or newer
"""""""""""""""""""""""""
pretix 4.4 introduces a new data structure to store historical financial data. If you already have existing
data in your database, you will need to back-fill this data or you might get incorrect reports! This is not
done automatically as part of the usual update steps since it can take a while on large databases and you might
want to do it in parallel while the system is already running again. Please execute the following command::
(venv)$ python -m pretix create_order_transactions
Or, with a docker installation::
$ docker exec -it pretix.service pretix create_order_transactions
.. _blog: https://pretix.eu/about/en/blog/

View File

@@ -25,6 +25,8 @@ description multi-lingual string A public descri
Markdown syntax or can be ``null``.
position integer An integer, used for sorting
require_membership boolean If ``true``, booking this variation requires an active membership.
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
be hidden from users without a valid membership.
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
sales_channels list of strings Sales channels this variation is available on, such as
``"web"`` or ``"resellers"``. Defaults to all existing sales channels.
@@ -75,6 +77,7 @@ Endpoints
},
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
@@ -95,6 +98,7 @@ Endpoints
},
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"description": {},
"position": 1,
@@ -144,6 +148,7 @@ Endpoints
"original_price": null,
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
@@ -179,6 +184,7 @@ Endpoints
"default_price": "10.00",
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
@@ -204,6 +210,7 @@ Endpoints
"original_price": null,
"active": true,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,
@@ -260,6 +267,7 @@ Endpoints
"original_price": null,
"active": false,
"require_membership": false,
"require_membership_hidden": false,
"require_membership_types": [],
"sales_channels": ["web"],
"available_from": null,

View File

@@ -70,6 +70,8 @@ require_approval boolean If ``true``, or
paid.
require_bundling boolean If ``true``, this item is only available as part of bundles.
require_membership boolean If ``true``, booking this item requires an active membership.
require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this product will
be hidden from users without a valid membership.
require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
grant_membership_type integer If set to the internal ID of a membership type, purchasing this item will
create a membership of the given type.
@@ -105,6 +107,8 @@ variations list of objects A list with one
├ active boolean If ``false``, this variation will not be sold or shown.
├ description multi-lingual string A public description of the variation. May contain
├ require_membership boolean If ``true``, booking this variation requires an active membership.
├ require_membership_hidden boolean If ``true`` and ``require_membership`` is set, this variation will
be hidden from users without a valid membership.
├ require_membership_types list of integers Internal IDs of membership types valid if ``require_membership`` is ``true``
Markdown syntax or can be ``null``.
├ sales_channels list of strings Sales channels this variation is available on, such as
@@ -155,6 +159,10 @@ meta_data object Values set for
The attributes ``require_membership``, ``require_membership_types``, ``grant_membership_type``, ``grant_membership_duration_like_event``,
``grant_membership_duration_days`` and ``grant_membership_duration_months`` have been added.
.. versionchanged:: 4.4
The attributes ``require_membership_hidden`` attribute has been added.
Notes
-----

View File

@@ -128,6 +128,10 @@ last_modified datetime Last modificati
The ``custom_followup_at`` attribute has been added.
.. versionchanged:: 4.4
The ``item`` and ``variation`` query parameters have been added.
.. _order-position-resource:
@@ -415,6 +419,8 @@ List of all orders
:query string code: Only return orders that match the given order code
:query string status: Only return orders in the given order status (see above)
:query string search: Only return orders matching a given search query
:query integer item: Only return orders with a position that contains this item ID. *Warning:* Result will also include orders if they contain mixed items, and it will even return orders where the item is only contained in a canceled position.
:query integer variation: Only return orders with a position that contains this variation ID. *Warning:* Result will also include orders if they contain mixed items and variations, and it will even return orders where the variation is only contained in a canceled position.
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
:query boolean require_approval: If set to ``true`` or ``false``, only categories with this value for the field
``require_approval`` will be returned.

View File

@@ -92,6 +92,9 @@ Carts and Orders
.. autoclass:: pretix.base.models.OrderRefund
:members:
.. autoclass:: pretix.base.models.Transaction
:members:
.. autoclass:: pretix.base.models.CartPosition
:members:

View File

@@ -17,6 +17,7 @@ bic
BIC
boolean
booleans
bugfix
cancelled
casted
Ceph
@@ -77,6 +78,7 @@ mixin
mixins
multi
multidomain
multiplicator
namespace
namespaced
namespaces

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "4.3.0"
__version__ = "4.4.1"

View File

@@ -704,6 +704,7 @@ class EventSettingsSerializer(SettingsSerializer):
'payment_term_accept_late',
'payment_explanation',
'payment_pending_hidden',
'mail_days_order_expire_warning',
'ticket_download',
'ticket_download_date',
'ticket_download_addons',

View File

@@ -59,7 +59,7 @@ class InlineItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types', 'available_from', 'available_until',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
@@ -75,7 +75,7 @@ class ItemVariationSerializer(I18nAwareModelSerializer):
model = ItemVariation
fields = ('id', 'value', 'active', 'description',
'position', 'default_price', 'price', 'original_price',
'require_membership', 'require_membership_types', 'available_from', 'available_until',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'available_from', 'available_until',
'sales_channels', 'hide_without_voucher',)
def __init__(self, *args, **kwargs):
@@ -175,7 +175,7 @@ class ItemSerializer(I18nAwareModelSerializer):
'min_per_order', 'max_per_order', 'checkin_attention', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'allow_waitinglist', 'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'grant_membership_type',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
'grant_membership_duration_like_event', 'grant_membership_duration_days',
'grant_membership_duration_months')
read_only_fields = ('has_variations',)

View File

@@ -1404,6 +1404,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
state=OrderPayment.PAYMENT_STATE_CREATED
)
order.create_transactions(is_new=True, fees=fees, positions=pos_map.values())
return order

View File

@@ -430,7 +430,13 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
if self.kwargs['pk'].isnumeric():
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
else:
op = queryset.get(secret=self.kwargs['pk'])
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
# scan apps still do it, so we try work around it!
try:
op = queryset.get(secret=self.kwargs['pk'])
except OrderPosition.DoesNotExist:
op = queryset.get(secret=self.kwargs['pk'].replace('+', ' '))
except OrderPosition.DoesNotExist:
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
if len(revoked_matches) == 0:

View File

@@ -92,6 +92,8 @@ with scopes_disabled():
subevent_after = django_filters.IsoDateTimeFilter(method='subevent_after_qs')
subevent_before = django_filters.IsoDateTimeFilter(method='subevent_before_qs')
search = django_filters.CharFilter(method='search_qs')
item = django_filters.CharFilter(field_name='all_positions', lookup_expr='item_id')
variation = django_filters.CharFilter(field_name='all_positions', lookup_expr='variation_id')
class Meta:
model = Order
@@ -214,7 +216,9 @@ class OrderViewSet(viewsets.ModelViewSet):
'positions',
opq.all().prefetch_related(
Prefetch('checkins', queryset=Checkin.objects.all()),
'item', 'variation', 'answers', 'answers__options', 'answers__question', 'seat',
'item', 'variation',
Prefetch('answers', queryset=QuestionAnswer.objects.prefetch_related('options', 'question').order_by('question__position')),
'seat',
)
)
)

View File

@@ -47,6 +47,7 @@ class PretixBaseConfig(AppConfig):
from . import notifications # NOQA
from . import email # NOQA
from .services import auth, checkin, export, mail, tickets, cart, orderimport, orders, invoices, cleanup, update_check, quotas, notifications, vouchers # NOQA
from .models import _transactions # NOQA
from django.conf import settings
try:

View File

@@ -539,6 +539,22 @@ def base_placeholders(sender, **kwargs):
'voucher_list', ['voucher_list'], lambda voucher_list: ' \n'.join(voucher_list),
' 68CYU2H6ZTP3WLK5\n 7MB94KKPVEPSMVF2'
),
SimpleFunctionalMailTextPlaceholder(
# join vouchers with two spaces at end of line so markdown-parser inserts a <br>
'voucher_url_list', ['event', 'voucher_list'],
lambda event, voucher_list: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in voucher_list
]),
lambda event: ' \n'.join([
build_absolute_uri(
event, 'presale:event.redeem'
) + '?voucher=' + c
for c in ['68CYU2H6ZTP3WLK5', '7MB94KKPVEPSMVF2']
]),
),
SimpleFunctionalMailTextPlaceholder(
'url', ['event', 'voucher_list'], lambda event, voucher_list: build_absolute_uri(event, 'presale:event.index', kwargs={
'event': event.slug,

View File

@@ -55,16 +55,20 @@ class JSONExporter(BaseExporter):
'name': str(self.event.organizer.name),
'slug': self.event.organizer.slug
},
'meta_data': self.event.meta_data,
'categories': [
{
'id': category.id,
'name': str(category.name),
'description': str(category.description),
'position': category.position,
'internal_name': category.internal_name
} for category in self.event.categories.all()
],
'items': [
{
'id': item.id,
'position': item.position,
'name': str(item.name),
'internal_name': str(item.internal_name),
'category': item.category_id,
@@ -73,13 +77,35 @@ class JSONExporter(BaseExporter):
'tax_name': str(item.tax_rule.name) if item.tax_rule else None,
'admission': item.admission,
'active': item.active,
'sales_channels': item.sales_channels,
'description': str(item.description),
'available_from': item.available_from,
'available_until': item.available_until,
'require_voucher': item.require_voucher,
'hide_without_voucher': item.hide_without_voucher,
'allow_cancel': item.allow_cancel,
'require_bundling': item.require_bundling,
'min_per_order': item.min_per_order,
'max_per_order': item.max_per_order,
'checkin_attention': item.checkin_attention,
'original_price': item.original_price,
'issue_giftcard': item.issue_giftcard,
'meta_data': item.meta_data,
'require_membership': item.require_membership,
'variations': [
{
'id': variation.id,
'active': variation.active,
'price': variation.default_price if variation.default_price is not None else
item.default_price,
'name': str(variation)
'name': str(variation),
'description': str(variation.description),
'position': variation.position,
'require_membership': variation.require_membership,
'sales_channels': variation.sales_channels,
'available_from': variation.available_from,
'available_until': variation.available_until,
'hide_without_voucher': variation.hide_without_voucher,
} for variation in item.variations.all()
]
} for item in self.event.items.select_related('tax_rule').prefetch_related('variations')
@@ -87,7 +113,13 @@ class JSONExporter(BaseExporter):
'questions': [
{
'id': question.id,
'identifier': question.identifier,
'required': question.required,
'question': str(question.question),
'position': question.position,
'hidden': question.hidden,
'ask_during_checkin': question.ask_during_checkin,
'help_text': str(question.help_text),
'type': question.type
} for question in self.event.questions.all()
],
@@ -95,7 +127,18 @@ class JSONExporter(BaseExporter):
{
'code': order.code,
'status': order.status,
'customer': order.customer.identifier if order.customer else None,
'testmode': order.testmode,
'user': order.email,
'email': order.email,
'phone': str(order.phone),
'locale': order.locale,
'comment': order.comment,
'custom_followup_at': order.custom_followup_at,
'require_approval': order.require_approval,
'checkin_attention': order.checkin_attention,
'sales_channel': order.sales_channel,
'expires': order.expires,
'datetime': order.datetime,
'fees': [
{
@@ -108,11 +151,21 @@ class JSONExporter(BaseExporter):
'positions': [
{
'id': position.id,
'positionid': position.positionid,
'item': position.item_id,
'variation': position.variation_id,
'subevent': position.subevent_id,
'seat': position.seat.seat_guid if position.seat else None,
'price': position.price,
'tax_rate': position.tax_rate,
'tax_value': position.tax_value,
'attendee_name': position.attendee_name,
'attendee_email': position.attendee_email,
'company': position.company,
'street': position.street,
'zipcode': position.zipcode,
'country': str(position.country) if position.country else None,
'state': position.state,
'secret': position.secret,
'addon_to': position.addon_to_id,
'answers': [
@@ -124,15 +177,30 @@ class JSONExporter(BaseExporter):
} for position in order.positions.all()
]
} for order in
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'fees')
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'positions__seat', 'customer', 'fees')
],
'quotas': [
{
'id': quota.id,
'size': quota.size,
'subevent': quota.subevent_id,
'items': [item.id for item in quota.items.all()],
'variations': [variation.id for variation in quota.variations.all()],
} for quota in self.event.quotas.all().prefetch_related('items', 'variations')
],
'subevents': [
{
'id': se.id,
'name': str(se.name),
'location': str(se.location),
'date_from': se.date_from,
'date_to': se.date_to,
'date_admission': se.date_admission,
'geo_lat': se.geo_lat,
'geo_lon': se.geo_lon,
'is_public': se.is_public,
'meta_data': se.meta_data,
} for se in self.event.subevents.all()
]
}
}

View File

@@ -33,6 +33,7 @@
# License for the specific language governing permissions and limitations under the License.
from collections import OrderedDict
from datetime import date, datetime, time
from decimal import Decimal
import dateutil
@@ -42,10 +43,10 @@ from django.db.models import (
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
Q, Subquery, Sum, When,
)
from django.db.models.functions import Coalesce, TruncDate
from django.db.models.functions import Coalesce
from django.dispatch import receiver
from django.utils.functional import cached_property
from django.utils.timezone import get_current_timezone, now
from django.utils.timezone import get_current_timezone, make_aware, now
from django.utils.translation import gettext as _, gettext_lazy, pgettext
from pretix.base.models import (
@@ -181,41 +182,43 @@ class OrderListExporter(MultiSheetListExporter):
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
annotations['date'] = TruncDate(f'{rel}datetime')
filters['date__gte'] = date_value
filters[f'{rel}datetime__gte'] = datetime_value
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
annotations['date'] = TruncDate(f'{rel}datetime')
filters['date__lte'] = date_value
filters[f'{rel}datetime__lte'] = datetime_value
if form_data.get('event_date_from'):
date_value = form_data.get('event_date_from')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
annotations['event_date_max'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Max(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_max__gte'] = date_value
filters['event_date_max__gte'] = datetime_value
if form_data.get('event_date_to'):
date_value = form_data.get('event_date_to')
if isinstance(date_value, str):
if not isinstance(date_value, date):
date_value = dateutil.parser.parse(date_value).date()
datetime_value = make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
annotations['event_date_min'] = Case(
When(**{f'{rel}event__has_subevents': True}, then=Min(f'{rel}all_positions__subevent__date_from')),
default=F(f'{rel}event__date_from'),
)
filters['event_date_min__lte'] = date_value
filters['event_date_min__lte'] = datetime_value
if filters:
return qs.annotate(**annotations).filter(**filters)
@@ -870,6 +873,78 @@ class QuotaListExporter(ListExporter):
return '{}_quotas'.format(self.event.slug)
def generate_GiftCardTransactionListExporter(organizer): # hackhack
class GiftcardTransactionListExporter(ListExporter):
identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions')
@property
def additional_form_fields(self):
d = [
('date_from',
forms.DateField(
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
('date_to',
forms.DateField(
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
)),
]
d = OrderedDict(d)
return d
def iterate_list(self, form_data):
qs = GiftCardTransaction.objects.filter(
card__issuer=organizer,
).order_by('datetime').select_related('card', 'order', 'order__event')
if form_data.get('date_from'):
date_value = form_data.get('date_from')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__gte=make_aware(datetime.combine(date_value, time(0, 0, 0)), self.timezone)
)
if form_data.get('date_to'):
date_value = form_data.get('date_to')
if isinstance(date_value, str):
date_value = dateutil.parser.parse(date_value).date()
qs = qs.filter(
datetime__lte=make_aware(datetime.combine(date_value, time(23, 59, 59, 999999)), self.timezone)
)
headers = [
_('Gift card code'),
_('Test mode'),
_('Date'),
_('Amount'),
_('Currency'),
_('Order'),
]
yield headers
for obj in qs:
row = [
obj.card.secret,
_('TEST MODE') if obj.card.testmode else '',
obj.datetime.astimezone(self.timezone).strftime('%Y-%m-%d %H:%M:%S'),
obj.value,
obj.card.currency,
obj.order.full_code if obj.order else None,
]
yield row
def get_filename(self):
return '{}_giftcardtransactions'.format(organizer.slug)
return GiftcardTransactionListExporter
class GiftcardRedemptionListExporter(ListExporter):
identifier = 'giftcardredemptionlist'
verbose_name = gettext_lazy('Gift card redemptions')
@@ -1062,3 +1137,8 @@ def register_multievent_i_giftcardredemptionlist_exporter(sender, **kwargs):
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardlist")
def register_multievent_i_giftcardlist_exporter(sender, **kwargs):
return generate_GiftCardListExporter(sender)
@receiver(register_multievent_data_exporters, dispatch_uid="multiexporter_giftcardtransactionlist")
def register_multievent_i_giftcardtransactionlist_exporter(sender, **kwargs):
return generate_GiftCardTransactionListExporter(sender)

View File

@@ -37,13 +37,10 @@ import json
import logging
from decimal import Decimal
from io import BytesIO
from urllib.error import HTTPError
import dateutil.parser
import pycountry
import pytz
import vat_moss.errors
import vat_moss.id
from babel import Locale
from django import forms
from django.conf import settings
@@ -52,7 +49,7 @@ from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db.models import QuerySet
from django.forms import Select
from django.forms import Select, widgets
from django.utils import translation
from django.utils.formats import date_format
from django.utils.html import escape
@@ -76,8 +73,9 @@ from pretix.base.i18n import (
get_babel_locale, get_language_without_region, language,
)
from pretix.base.models import InvoiceAddress, Question, QuestionOption
from pretix.base.models.tax import (
EU_COUNTRIES, cc_to_vat_prefix, is_eu_country,
from pretix.base.models.tax import VAT_ID_COUNTRIES, ask_for_vat_id
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, PERSON_NAME_SALUTATIONS,
@@ -154,8 +152,9 @@ class NamePartsWidget(forms.MultiWidget):
final_attrs,
id='%s_%s' % (id_, i),
title=self.scheme['fields'][i][1],
placeholder=self.scheme['fields'][i][1],
)
if not isinstance(widget, widgets.Select):
these_attrs['placeholder'] = self.scheme['fields'][i][1]
if self.scheme['fields'][i][0] in REQUIRED_NAME_PARTS:
if self.field.required:
these_attrs['required'] = 'required'
@@ -901,7 +900,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
'data-display-dependency': '#id_is_business_1',
'autocomplete': 'organization',
}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-in-eu': ','.join(EU_COUNTRIES)}),
'vat_id': forms.TextInput(attrs={'data-display-dependency': '#id_is_business_1', 'data-countries-with-vat-id': ','.join(VAT_ID_COUNTRIES)}),
'internal_reference': forms.TextInput,
}
labels = {
@@ -921,6 +920,18 @@ class BaseInvoiceAddressForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if not event.settings.invoice_address_vatid:
del self.fields['vat_id']
elif self.validate_vat_id:
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but depending on the country you reside in we might need to charge you '
'additional taxes if you do not enter it.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
else:
self.fields['vat_id'].help_text = '<br/>'.join([
str(_('Optional, but it might be required for you to claim tax benefits on your invoice '
'depending on your and the sellers country of residence.')),
str(_('If you are registered in Switzerland, you can enter your UID instead.')),
])
self.fields['country'].choices = CachedCountries()
@@ -952,7 +963,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['state'].widget.is_required = True
# Without JavaScript the VAT ID field is not hidden, so we empty the field if a country outside the EU is selected.
if cc and not is_eu_country(cc) and fprefix + 'vat_id' in self.data:
if cc and not ask_for_vat_id(cc) and fprefix + 'vat_id' in self.data:
self.data = self.data.copy()
del self.data[fprefix + 'vat_id']
@@ -1002,7 +1013,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
if not data.get('is_business'):
data['company'] = ''
data['vat_id'] = ''
if data.get('is_business') and not is_eu_country(data.get('country')):
if data.get('is_business') and not ask_for_vat_id(data.get('country')):
data['vat_id'] = ''
if self.event.settings.invoice_address_required:
if data.get('is_business') and not data.get('company'):
@@ -1025,36 +1036,19 @@ class BaseInvoiceAddressForm(forms.ModelForm):
# Do not save the country if it is the only field set -- we don't know the user even checked it!
self.cleaned_data['country'] = ''
if data.get('vat_id') and is_eu_country(data.get('country')) and data.get('vat_id')[:2] != cc_to_vat_prefix(str(data.get('country'))):
raise ValidationError(_('Your VAT ID does not match the selected country.'))
if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass
elif self.validate_vat_id and data.get('is_business') and is_eu_country(data.get('country')) and data.get('vat_id'):
elif self.validate_vat_id and data.get('is_business') and ask_for_vat_id(data.get('country')) and data.get('vat_id'):
try:
result = vat_moss.id.validate(data.get('vat_id'))
if result:
country_code, normalized_id, company_name = result
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
except (vat_moss.errors.InvalidError, ValueError):
raise ValidationError(_('This VAT ID is not valid. Please re-check your input.'))
except vat_moss.errors.WebServiceUnavailableError:
logger.exception('VAT ID checking failed for country {}'.format(data.get('country')))
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = True
self.instance.vat_id = normalized_id
except VATIDFinalError as e:
raise ValidationError(e.message)
except VATIDTemporaryError as e:
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 is currently not available. We will therefore '
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'))
except (vat_moss.errors.WebServiceError, HTTPError):
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.'))
messages.warning(self.request, e.message)
else:
self.instance.vat_id_validated = False

View File

@@ -595,7 +595,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
table.setStyle(TableStyle(tstyledata))
story.append(table)
story.append(Spacer(1, 15 * mm))
story.append(Spacer(1, 10 * mm))
if self.invoice.payment_provider_text:
story.append(Paragraph(
@@ -611,12 +611,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.invoice.additional_text,
self.stylesheet['Normal']
))
story.append(Spacer(1, 15 * mm))
story.append(Spacer(1, 5 * mm))
tstyledata = [
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
('LEFTPADDING', (0, 0), (0, -1), 0),
('RIGHTPADDING', (-1, 0), (-1, -1), 0),
('TOPPADDING', (0, 0), (-1, -1), 1),
('BOTTOMPADDING', (0, 0), (-1, -1), 1),
('FONTSIZE', (0, 0), (-1, -1), 8),
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
]

View File

@@ -0,0 +1,82 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from decimal import Decimal
from django.core.management.base import BaseCommand
from django.db import models
from django.db.models import Case, F, OuterRef, Q, Subquery, Sum, Value, When
from django.db.models.functions import Coalesce
from django_scopes import scopes_disabled
from pretix.base.models import Order, OrderFee, OrderPosition
from pretix.base.models.orders import Transaction
class Command(BaseCommand):
help = "Check order for consistency with their transactions"
@scopes_disabled()
def handle(self, *args, **options):
qs = Order.objects.annotate(
position_total=Coalesce(
Subquery(
OrderPosition.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('price')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
fee_total=Coalesce(
Subquery(
OrderFee.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum('value')).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
tx_total=Coalesce(
Subquery(
Transaction.objects.filter(
order=OuterRef('pk')
).order_by().values('order').annotate(p=Sum(F('price') * F('count'))).values('p'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
), Value(0), output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).annotate(
correct_total=Case(
When(Q(status=Order.STATUS_CANCELED) | Q(status=Order.STATUS_EXPIRED) | Q(require_approval=True),
then=Value(0)),
default=F('position_total') + F('fee_total'),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
).exclude(
total=F('position_total') + F('fee_total'),
tx_total=F('correct_total')
).select_related('event')
for o in qs:
if abs(o.tx_total - o.correct_total) < Decimal('0.00001') and abs(o.position_total + o.fee_total - o.total) < Decimal('0.00001'):
# Ignore SQLite which treats Decimals like floats…
continue
print(f"Error in order {o.full_code}: status={o.status}, sum(positions)+sum(fees)={o.position_total + o.fee_total}, "
f"order.total={o.total}, sum(transactions)={o.tx_total}, expected={o.correct_total}")
self.stderr.write(self.style.SUCCESS(f'Check completed.'))

View File

@@ -0,0 +1,95 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import time
from django.core.management.base import BaseCommand
from django.db.models import F, Max, Q
from django.utils.timezone import now
from django_scopes import scopes_disabled
from tqdm import tqdm
from pretix.base.models import Order
class Command(BaseCommand):
help = "Create missing order transactions"
def add_arguments(self, parser):
parser.add_argument(
"--slowdown",
dest="interval",
type=int,
default=0,
help="Interval for staggered execution. If set to a value different then zero, we will "
"wait this many milliseconds between every order we process.",
)
@scopes_disabled()
def handle(self, *args, **options):
t = 0
qs = Order.objects.annotate(
last_transaction=Max('transactions__created')
).filter(
Q(last_transaction__isnull=True) | Q(last_modified__gt=F('last_transaction')),
require_approval=False,
).prefetch_related(
'all_positions', 'all_fees'
).order_by(
'pk'
)
last_pk = 0
with tqdm(total=qs.count()) as pbar:
while True:
batch = list(qs.filter(pk__gt=last_pk)[:5000])
if not batch:
break
for o in batch:
if o.last_transaction is None:
tn = o.create_transactions(
positions=o.all_positions.all(),
fees=o.all_fees.all(),
dt_now=o.datetime,
migrated=True,
is_new=True,
_backfill_before_cancellation=True,
)
o.create_transactions(
positions=o.all_positions.all(),
fees=o.all_fees.all(),
dt_now=o.cancellation_date or (o.expires if o.status == Order.STATUS_EXPIRED else o.datetime),
migrated=True,
)
else:
tn = o.create_transactions(
positions=o.all_positions.all(),
fees=o.all_fees.all(),
dt_now=now(),
migrated=True,
)
if tn:
t += 1
time.sleep(0)
pbar.update(1)
last_pk = batch[-1].pk
self.stderr.write(self.style.SUCCESS(f'Created transactions for {t} orders.'))

View File

@@ -0,0 +1,21 @@
# Generated by Django 3.2.4 on 2021-09-30 10:25
from datetime import datetime
from django.db import migrations, models
from pytz import UTC
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0197_auto_20210914_0814'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='sent_to_customer',
field=models.DateTimeField(blank=True, null=True, default=UTC.localize(datetime(1970, 1, 1, 0, 0, 0, 0))),
preserve_default=False,
),
]

View File

@@ -0,0 +1,23 @@
# Generated by Django 3.2.4 on 2021-10-05 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0198_invoice_sent_to_customer'),
]
operations = [
migrations.AddField(
model_name='item',
name='require_membership_hidden',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='itemvariation',
name='require_membership_hidden',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,38 @@
# Generated by Django 3.2.4 on 2021-10-18 10:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0199_auto_20211005_1050'),
]
operations = [
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, db_index=True)),
('datetime', models.DateTimeField(db_index=True)),
('migrated', models.BooleanField(default=False)),
('positionid', models.PositiveIntegerField(default=1, null=True)),
('count', models.IntegerField(default=1)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7)),
('tax_value', models.DecimalField(decimal_places=2, max_digits=10)),
('fee_type', models.CharField(max_length=100, null=True)),
('internal_type', models.CharField(max_length=255, null=True)),
('item', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.item')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to='pretixbase.order')),
('subevent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.subevent')),
('tax_rule', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.taxrule')),
('variation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.itemvariation')),
],
options={
'ordering': ('datetime', 'pk'),
},
),
]

View File

@@ -42,8 +42,9 @@ from .notifications import NotificationSetting
from .orders import (
AbstractPosition, CachedCombinedTicket, CachedTicket, CartPosition,
InvoiceAddress, Order, OrderFee, OrderPayment, OrderPosition, OrderRefund,
QuestionAnswer, RevokedTicketSecret, cachedcombinedticket_name,
cachedticket_name, generate_position_secret, generate_secret,
QuestionAnswer, RevokedTicketSecret, Transaction,
cachedcombinedticket_name, cachedticket_name, generate_position_secret,
generate_secret,
)
from .organizer import (
Organizer, Organizer_SettingsStore, Team, TeamAPIToken, TeamInvite,

View File

@@ -0,0 +1,113 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
"""
This module contains helper functions that are supposed to call out code paths missing calls to
``Order.create_transaction()`` by actively breaking them. Read the docstring of the ``Transaction`` class for a
detailed reasoning why this exists.
"""
import inspect
import logging
import os
import threading
from django.conf import settings
from django.db import transaction
dirty_transactions = threading.local()
logger = logging.getLogger(__name__)
fail_loudly = os.getenv('PRETIX_DIRTY_TRANSACTIONS_QUIET', 'false' if settings.DEBUG else 'true') not in ('true', 'True', 'on', '1')
class DirtyTransactionsForOrderException(Exception):
pass
def _fail(message):
if fail_loudly:
raise DirtyTransactionsForOrderException(message)
else:
if settings.SENTRY_ENABLED:
import sentry_sdk
sentry_sdk.capture_message(message, "fatal")
logger.warning(message, stack_info=True)
def _check_for_dirty_orders():
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
try:
if dirty_transactions.order_ids and dirty_transactions.order_ids != {None}:
_fail(
f"In the transaction that just ended, you created or modified an Order, OrderPosition, or OrderFee "
f"object in a way that you should have called `order.create_transactions()` afterwards. The transaction "
f"still went through and your data can be fixed with the `create_order_transactions` management command "
f"but you should update your code to prevent this from happening. Affected order IDs: {dirty_transactions.order_ids}"
)
finally:
dirty_transactions.order_ids.clear()
def _transactions_mark_order_dirty(order_id, using=None):
if "PYTEST_CURRENT_TEST" in os.environ:
# We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code
# or not.
for frame in inspect.stack():
if 'pretix/base/models/orders' in frame.filename:
continue
elif 'test_' in frame.filename or 'conftest.py in frame.filename':
return
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
# This went through non-test code, let's consider it non-test
break
if order_id is None:
return
conn = transaction.get_connection(using)
if not conn.in_atomic_block:
_fail(
"You modified an Order, OrderPosition, or OrderFee object in a way that should create "
"a new Transaction object within the same database transaction, however you are not "
"doing it inside a database transaction!"
)
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
if _check_for_dirty_orders not in [func for savepoint_id, func in conn.run_on_commit]:
transaction.on_commit(_check_for_dirty_orders, using)
dirty_transactions.order_ids.clear() # This is necessary to clean up after old threads with rollbacked transactions
dirty_transactions.order_ids.add(order_id)
def _transactions_mark_order_clean(order_id):
if getattr(dirty_transactions, 'order_ids', None) is None:
dirty_transactions.order_ids = set()
try:
dirty_transactions.order_ids.remove(order_id)
except KeyError:
pass

View File

@@ -25,6 +25,7 @@ from django.contrib.auth.hashers import (
check_password, is_password_usable, make_password,
)
from django.db import models
from django.db.models import F, Q
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django_scopes import ScopedManager, scopes_disabled
@@ -183,6 +184,12 @@ class Customer(LoggedModel):
def stored_addresses(self):
return self.invoice_addresses(manager='profiles')
def usable_memberships(self, for_event, testmode=False):
return self.memberships.active(for_event).with_usages().filter(
Q(membership_type__max_usages__isnull=True) | Q(usages__lt=F('membership_type__max_usages')),
testmode=testmode,
)
class AttendeeProfile(models.Model):
customer = models.ForeignKey(

View File

@@ -57,6 +57,7 @@ from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext, gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
@@ -145,7 +146,7 @@ class EventMixin:
("SHORT_" if short else "") + ("DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT")
)
def get_date_range_display(self, tz=None, force_show_end=False) -> str:
def get_date_range_display(self, tz=None, force_show_end=False, as_html=False) -> str:
"""
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_date_to``
@@ -153,8 +154,17 @@ class EventMixin:
"""
tz = tz or self.timezone
if (not self.settings.show_date_to and not force_show_end) or not self.date_to:
if as_html:
return format_html(
"<time datetime=\"{}\">{}</time>",
_date(self.date_from.astimezone(tz), "Y-m-d"),
_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), as_html)
def get_date_range_display_as_html(self, tz=None, force_show_end=False) -> str:
return self.get_date_range_display(tz, force_show_end, as_html=True)
def get_time_range_display(self, tz=None, force_show_end=False) -> str:
"""
@@ -1421,7 +1431,7 @@ class SubEvent(EventMixin, LoggedModel):
return self.event.currency
def allow_delete(self):
return not self.orderposition_set.exists()
return not self.orderposition_set.exists() and not self.transaction_set.exists()
def delete(self, *args, **kwargs):
clear_cache = kwargs.pop('clear_cache', False)

View File

@@ -159,6 +159,8 @@ class Invoice(models.Model):
# False: The invoice wasn't sent and never will, because sending was not configured at the time of the check.
sent_to_organizer = models.BooleanField(null=True, blank=True)
sent_to_customer = models.DateTimeField(null=True, blank=True)
file = models.FileField(null=True, blank=True, upload_to=invoice_filename, max_length=255)
objects = ScopedManager(organizer='event__organizer')
@@ -235,7 +237,7 @@ class Invoice(models.Model):
def _get_invoice_number_from_order(self):
return '{order}-{count}'.format(
order=self.order.code,
count=Invoice.objects.filter(event=self.event, order=self.order).count() + 1,
count=Invoice.objects.filter(event=self.event, prefix=self.prefix, invoice_no__startswith=f"{self.order.code}-", order=self.order).count() + 1,
)
def save(self, *args, **kwargs):
@@ -300,6 +302,9 @@ class Invoice(models.Model):
def __repr__(self):
return '<Invoice {} / {}>'.format(self.full_invoice_no, self.pk)
def __str__(self):
return self.full_invoice_no
class InvoiceLine(models.Model):
"""

View File

@@ -523,6 +523,12 @@ class Item(LoggedModel):
verbose_name=_('Allowed membership types'),
blank=True,
)
require_membership_hidden = models.BooleanField(
verbose_name=_('Hide without a valid membership'),
help_text=_('Do not show this unless the customer is logged in and has a valid membership. Be aware that '
'this means it will never be visible in the widget.'),
default=False,
)
grant_membership_type = models.ForeignKey(
'MembershipType',
null=True, blank=True,
@@ -687,9 +693,9 @@ class Item(LoggedModel):
return res
def allow_delete(self):
from pretix.base.models.orders import OrderPosition
from pretix.base.models.orders import OrderPosition, Transaction
return not OrderPosition.all.filter(item=self).exists()
return not Transaction.objects.filter(item=self).exists() and not OrderPosition.all.filter(item=self).exists()
@property
def includes_mixed_tax_rate(self):
@@ -802,6 +808,12 @@ class ItemVariation(models.Model):
verbose_name=_('Membership types'),
blank=True,
)
require_membership_hidden = models.BooleanField(
verbose_name=_('Hide without a valid membership'),
help_text=_('Do not show this unless the customer is logged in and has a valid membership. Be aware that '
'this means it will never be visible in the widget.'),
default=False,
)
available_from = models.DateTimeField(
verbose_name=_("Available from"),
null=True, blank=True,
@@ -946,10 +958,13 @@ class ItemVariation(models.Model):
return self.position < other.position
def allow_delete(self):
from pretix.base.models.orders import CartPosition, OrderPosition
from pretix.base.models.orders import (
CartPosition, OrderPosition, Transaction,
)
return (
not OrderPosition.objects.filter(variation=self).exists()
not Transaction.objects.filter(variation=self).exists()
and not OrderPosition.objects.filter(variation=self).exists()
and not CartPosition.objects.filter(variation=self).exists()
)

View File

@@ -95,6 +95,7 @@ class MembershipQuerySet(models.QuerySet):
def active(self, ev):
return self.filter(
canceled=False,
date_start__lte=ev.date_from,
date_end__gte=ev.date_from
)
@@ -175,7 +176,7 @@ class Membership(models.Model):
else:
dt = now()
return dt >= self.date_start and dt <= self.date_end
return not self.canceled and dt >= self.date_start and dt <= self.date_end
def allow_delete(self):
return self.testmode and not self.orderposition_set.exists()

View File

@@ -80,6 +80,9 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import order_gracefully_delete
from ...helpers.countries import CachedCountries, FastCountryField
from ._transactions import (
_fail, _transactions_mark_order_clean, _transactions_mark_order_dirty,
)
from .base import LockModel, LoggedModel
from .event import Event, SubEvent
from .items import Item, ItemVariation, Question, QuestionOption, Quota
@@ -262,6 +265,11 @@ class Order(LockModel, LoggedModel):
def __str__(self):
return self.full_code
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'require_approval' not in self.get_deferred_fields() and 'status' not in self.get_deferred_fields():
self.__initial_status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
def gracefully_delete(self, user=None, auth=None):
from . import GiftCard, GiftCardTransaction, Membership, Voucher
@@ -289,6 +297,7 @@ class Order(LockModel, LoggedModel):
OrderPosition.all.filter(order=self, addon_to__isnull=False).delete()
OrderPosition.all.filter(order=self).delete()
OrderFee.all.filter(order=self).delete()
Transaction.objects.filter(order=self).delete()
self.refunds.all().delete()
self.payments.all().delete()
self.event.cache.delete('complain_testmode_orders')
@@ -444,7 +453,27 @@ class Order(LockModel, LoggedModel):
self.datetime = now()
if not self.expires:
self.set_expires()
super().save(**kwargs)
is_new = not self.pk
update_fields = kwargs.get('update_fields', [])
if 'require_approval' not in self.get_deferred_fields() and 'status' not in self.get_deferred_fields():
status_paid_or_pending = self.status in (Order.STATUS_PENDING, Order.STATUS_PAID) and not self.require_approval
if status_paid_or_pending != self.__initial_status_paid_or_pending:
_transactions_mark_order_dirty(self.pk, using=kwargs.get('using', None))
elif (
not kwargs.get('force_save_with_deferred_fields', None) and
(not update_fields or ('require_approval' not in update_fields and 'status' not in update_fields))
):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
r = super().save(**kwargs)
if is_new:
_transactions_mark_order_dirty(self.pk, using=kwargs.get('using', None))
return r
def touch(self):
self.save(update_fields=['last_modified'])
@@ -999,6 +1028,59 @@ class Order(LockModel, LoggedModel):
continue
yield op
def create_transactions(self, is_new=False, positions=None, fees=None, dt_now=None, migrated=False,
_backfill_before_cancellation=False, save=True):
dt_now = dt_now or now()
# Count the transactions we already have
current_transaction_count = Counter()
if not is_new:
for t in Transaction.objects.filter(order=self): # do not use related manager, we want to avoid cached data
current_transaction_count[Transaction.key(t)] += t.count
# Count the transactions we'd actually need
target_transaction_count = Counter()
if (_backfill_before_cancellation or self.status in (Order.STATUS_PENDING, Order.STATUS_PAID)) and not self.require_approval:
positions = self.positions.all() if positions is None else positions
for p in positions:
if p.canceled and not _backfill_before_cancellation:
continue
target_transaction_count[Transaction.key(p)] += 1
fees = self.fees.all() if fees is None else fees
for f in fees:
if f.canceled and not _backfill_before_cancellation:
continue
target_transaction_count[Transaction.key(f)] += 1
keys = set(target_transaction_count.keys()) | set(current_transaction_count.keys())
create = []
for k in keys:
positionid, itemid, variationid, subeventid, price, taxrate, taxruleid, taxvalue, feetype, internaltype = k
d = target_transaction_count[k] - current_transaction_count[k]
if d:
create.append(Transaction(
order=self,
datetime=dt_now,
migrated=migrated,
positionid=positionid,
count=d,
item_id=itemid,
variation_id=variationid,
subevent_id=subeventid,
price=price,
tax_rate=taxrate,
tax_rule_id=taxruleid,
tax_value=taxvalue,
fee_type=feetype,
internal_type=internaltype,
))
create.sort(key=lambda t: (0 if t.count < 0 else 1, t.positionid or 0))
if save:
Transaction.objects.bulk_create(create)
_transactions_mark_order_clean(self.pk)
return create
def answerfile_name(instance, filename: str) -> str:
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
@@ -1468,6 +1550,7 @@ class OrderPayment(models.Model):
'message': can_be_paid
}, user=user, auth=auth)
raise Quota.QuotaExceededException(can_be_paid)
status_change = self.order.status != Order.STATUS_PENDING
self.order.status = Order.STATUS_PAID
self.order.save(update_fields=['status'])
@@ -1481,6 +1564,8 @@ class OrderPayment(models.Model):
if overpaid:
self.order.log_action('pretix.event.order.overpaid', {}, user=user, auth=auth)
order_paid.send(self.order.event, order=self.order)
if status_change:
self.order.create_transactions()
def fail(self, info=None, user=None, auth=None):
"""
@@ -1958,6 +2043,12 @@ class OrderFee(models.Model):
def net_value(self):
return self.value - self.tax_value
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.get_deferred_fields():
self.__initial_transaction_key = Transaction.key(self)
self.__initial_canceled = self.canceled
def __str__(self):
if self.description:
return '{} - {}'.format(self.get_fee_type_display(), self.description)
@@ -1996,6 +2087,15 @@ class OrderFee(models.Model):
if self.tax_rate is None:
self._calculate_tax()
self.order.touch()
if not self.get_deferred_fields():
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
return super().save(*args, **kwargs)
def delete(self, **kwargs):
@@ -2010,7 +2110,7 @@ class OrderPosition(AbstractPosition):
AbstractPosition.
The default ``OrderPosition.objects`` manager only contains fees that are not ``canceled``. If
you ant all objects, you need to use ``OrderPosition.all`` instead.
you want all objects, you need to use ``OrderPosition.all`` instead.
:param order: The order this position is a part of
:type order: Order
@@ -2061,6 +2161,12 @@ class OrderPosition(AbstractPosition):
all = ScopedManager(organizer='order__event__organizer')
objects = ActivePositionManager()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.get_deferred_fields():
self.__initial_transaction_key = Transaction.key(self)
self.__initial_canceled = self.canceled
class Meta:
verbose_name = _("Order position")
verbose_name_plural = _("Order positions")
@@ -2104,6 +2210,7 @@ class OrderPosition(AbstractPosition):
op._calculate_tax()
op.positionid = i + 1
op.save()
ops.append(op)
cp_mapping[cartpos.pk] = op
for answ in cartpos.answers.all():
answ.orderposition = op
@@ -2169,6 +2276,14 @@ class OrderPosition(AbstractPosition):
if not self.pseudonymization_id:
self.assign_pseudonymization_id()
if not self.get_deferred_fields():
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
return super().save(*args, **kwargs)
@scopes_disabled()
@@ -2212,7 +2327,7 @@ class OrderPosition(AbstractPosition):
:param attach_ical: Attach relevant ICS files
"""
from pretix.base.services.mail import (
SendMailException, mail, render_mail,
SendMailException, TolerantDict, mail, render_mail,
)
if not self.attendee_email:
@@ -2225,6 +2340,7 @@ class OrderPosition(AbstractPosition):
recipient = self.attendee_email
try:
email_content = render_mail(template, context)
subject = str(subject).format_map(TolerantDict(context))
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
@@ -2263,6 +2379,151 @@ class OrderPosition(AbstractPosition):
)
class Transaction(models.Model):
"""
Transactions are a data structure that is redundant on the first sight but makes it possible to create good
financial reporting.
To understand this, think of "orders" as something like a contractual relationship between the organizer and the
customer which requires to customer to pay some money and the organizer to provide a ticket.
The ``Order``, ``OrderPosition``, and ``OrderFee`` models combined give a representation of the current contractual
status of this relationship, i.e. how much and what is owed. The ``OrderPayment`` and ``OrderRefund`` models indicate
the "other side" of the relationship, i.e. how much of the financial obligation has been met so far.
However, while ``OrderPayment`` and ``OrderRefund`` objects are "final" and no longer change once they reached their
final state, ``Order``, ``OrderPosition`` and ``OrderFee`` are highly mutable and can change at any time, e.g. if
the customer moves their booking to a different day or a discount is applied retroactively.
Therefore those models can be used to answer the question "how many tickets of type X have been sold for my event
as of today?" but they cannot accurately answer the question "how many tickets of type X have been sold for my event
as of last month?" because they lack this kind of historical information.
Transactions help here because they are "immutable copies" or "modification records" of call positions and fees
at the time of their creation and change. They only record data that is usually relevant for financial reporting,
such as amounts, prices, products and dates involved. They do not record data like attendee names etc.
Even before the introduction of the Transaction Model pretix *did* store historical data for auditability in the
LogEntry model. However, it's almost impossible to do efficient reporting on that data.
Transactions should never be generated manually but only through the ``order.create_transactions()``
method which should be called **within the same database transaction**.
The big downside of this approach is that you need to remember to update transaction records every time you change
or create orders in new code paths. The mechanism introduced in ``pretix.base.models._transactions`` as well as
the ``save()`` methods of ``Order``, ``OrderPosition`` and ``OrderFee`` intends to help you notice if you missed
it. The only thing this *doesn't* catch is usage of ``OrderPosition.objects.bulk_create`` (and likewise for
``bulk_update`` and ``OrderFee``).
:param id: ID of the transaction
:param order: Order the transaction belongs to
:param datetime: Date and time of the transaction
:param migrated: Whether this object was reconstructed because the order was created before transactions where introduced
:param positionid: Affected Position ID, in case this transaction represents a change in an order position
:param count: An amount, multiplicator for price etc. For order positions this can *currently* only be -1 or +1, for
fees it can also be more in special cases
:param item: ``Item``, in case this transaction represents a change in an order position
:param variation: ``ItemVariation``, in case this transaction represents a change in an order position
:param subevent: ``subevent``, in case this transaction represents a change in an order position
:param price: Price of the changed position
:param tax_rate: Tax rate of the changed position
:param tax_rule: Used tax rule
:param tax_value: Tax value in event currency
:param fee_type: Fee type code in case this transaction represents a change in an order fee
:param internal_type: Internal fee type in case this transaction represents a change in an order fee
"""
id = models.BigAutoField(primary_key=True)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
related_name='transactions',
on_delete=models.PROTECT
)
created = models.DateTimeField(
auto_now_add=True,
db_index=True,
)
datetime = models.DateTimeField(
verbose_name=_("Date"),
db_index=True,
)
migrated = models.BooleanField(
default=False
)
positionid = models.PositiveIntegerField(default=1, null=True, blank=True)
count = models.IntegerField(
default=1
)
item = models.ForeignKey(
Item,
null=True, blank=True,
verbose_name=_("Item"),
on_delete=models.PROTECT
)
variation = models.ForeignKey(
ItemVariation,
null=True, blank=True,
verbose_name=_("Variation"),
on_delete=models.PROTECT
)
subevent = models.ForeignKey(
SubEvent,
null=True, blank=True,
on_delete=models.PROTECT,
verbose_name=pgettext_lazy("subevent", "Date"),
)
price = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Price")
)
tax_rate = models.DecimalField(
max_digits=7, decimal_places=2,
verbose_name=_('Tax rate')
)
tax_rule = models.ForeignKey(
'TaxRule',
on_delete=models.PROTECT,
null=True, blank=True
)
tax_value = models.DecimalField(
max_digits=10, decimal_places=2,
verbose_name=_('Tax value')
)
fee_type = models.CharField(
max_length=100, choices=OrderFee.FEE_TYPES, null=True, blank=True
)
internal_type = models.CharField(max_length=255, null=True, blank=True)
class Meta:
ordering = 'datetime', 'pk'
def save(self, *args, **kwargs):
if not self.fee_type and not self.item:
raise ValidationError('Should set either item or fee type')
return super().save(*args, **kwargs)
@staticmethod
def key(obj):
if isinstance(obj, Transaction):
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
elif isinstance(obj, OrderPosition):
return (obj.positionid, obj.item_id, obj.variation_id, obj.subevent_id, obj.price, obj.tax_rate,
obj.tax_rule_id, obj.tax_value, None, None)
elif isinstance(obj, OrderFee):
return (None, None, None, None, obj.value, obj.tax_rate,
obj.tax_rule_id, obj.tax_value, obj.fee_type, obj.internal_type)
raise ValueError('invalid state') # noqa
@property
def full_price(self):
return self.price * self.count
@property
def full_tax_value(self):
return self.tax_value * self.count
class CartPosition(AbstractPosition):
"""
A cart position is similar to an order line, except that it is not
@@ -2351,8 +2612,7 @@ class InvoiceAddress(models.Model):
country = FastCountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'),
countries=CachedCountries)
state = models.CharField(max_length=255, verbose_name=pgettext_lazy('address', 'State'), blank=True)
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
help_text=_('Only for business customers within the EU.'))
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'))
vat_id_validated = models.BooleanField(default=False)
custom_field = models.CharField(max_length=255, null=True, blank=True)
internal_reference = models.TextField(

View File

@@ -25,7 +25,6 @@ from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.formats import localize
from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _, pgettext
from i18nfield.fields import I18nCharField
from i18nfield.strings import LazyI18nString
@@ -93,7 +92,7 @@ TAXED_ZERO = TaxedPrice(
EU_COUNTRIES = {
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT',
'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB'
'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE',
}
EU_CURRENCIES = {
'BG': 'BGN',
@@ -106,17 +105,21 @@ EU_CURRENCIES = {
'RO': 'RON',
'SE': 'SEK'
}
VAT_ID_COUNTRIES = EU_COUNTRIES | {'CH'}
def is_eu_country(cc):
cc = str(cc)
if cc == 'GB':
return now().astimezone(get_current_timezone()).year <= 2020
else:
return cc in EU_COUNTRIES
return cc in EU_COUNTRIES
def ask_for_vat_id(cc):
cc = str(cc)
return cc in VAT_ID_COUNTRIES
def cc_to_vat_prefix(country_code):
country_code = str(country_code)
if country_code == 'GR':
return 'EL'
return country_code
@@ -162,10 +165,13 @@ class TaxRule(LoggedModel):
pass
def allow_delete(self):
from pretix.base.models.orders import OrderFee, OrderPosition
from pretix.base.models.orders import (
OrderFee, OrderPosition, Transaction,
)
return (
not OrderFee.objects.filter(tax_rule=self, order__event=self.event).exists()
not Transaction.objects.filter(tax_rule=self, order__event=self.event).exists()
and not OrderFee.objects.filter(tax_rule=self, order__event=self.event).exists()
and not OrderPosition.all.filter(tax_rule=self, order__event=self.event).exists()
and not self.event.items.filter(tax_rule=self).exists()
and self.event.settings.tax_rate_default != self

View File

@@ -291,6 +291,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True):
cancellation.payment_provider_text = ''
cancellation.file = None
cancellation.sent_to_organizer = None
cancellation.sent_to_customer = None
with language(invoice.locale, invoice.event.settings.region):
cancellation.invoice_from = invoice.event.settings.get('invoice_address_from')
cancellation.invoice_from_name = invoice.event.settings.get('invoice_address_from_name')
@@ -346,8 +347,8 @@ def invoice_pdf_task(invoice: int):
i.file.delete()
with language(i.locale, i.event.settings.region):
fname, ftype, fcontent = i.event.invoice_renderer.generate(i)
i.file.save(fname, ContentFile(fcontent))
i.save()
i.file.save(fname, ContentFile(fcontent), save=False)
i.save(update_fields=['file'])
return i.file.name

View File

@@ -32,7 +32,7 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import hashlib
import inspect
import logging
import os
@@ -57,7 +57,7 @@ from django.core.mail import (
from django.core.mail.message import SafeMIMEText
from django.db import transaction
from django.template.loader import get_template
from django.utils.timezone import override
from django.utils.timezone import now, override
from django.utils.translation import gettext as _, pgettext
from django_scopes import scope, scopes_disabled
from i18nfield.strings import LazyI18nString
@@ -438,6 +438,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
email = email_filter.send_chained(event, 'message', message=email, order=order, user=user)
invoices_sent = []
if invoices:
invoices = Invoice.objects.filter(pk__in=invoices)
for inv in invoices:
@@ -449,6 +450,7 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
inv.file.file.read(),
'application/pdf'
)
invoices_sent.append(inv)
except:
logger.exception('Could not attach invoice to email')
pass
@@ -472,10 +474,30 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
try:
backend.send_messages([email])
except (smtplib.SMTPResponseException, smtplib.SMTPSenderRefused) as e:
if e.smtp_code in (101, 111, 421, 422, 431, 442, 447, 452):
# Most likely temporary, retry again (but pretty soon)
if e.smtp_code in (101, 111, 421, 422, 431, 432, 442, 447, 452):
if e.smtp_code == 432 and settings.HAS_REDIS:
# This is likely Microsoft Exchange Online which has a pretty bad rate limit of max. 3 concurrent
# SMTP connections which is *easily* exceeded with many celery threads. Just retrying with exponential
# backoff won't be good enough if we have a lot of emails, instead we'll need to make sure our retry
# intervals scatter such that the email won't all be retried at the same time again and cause the
# same problem.
# See also https://docs.microsoft.com/en-us/exchange/troubleshoot/send-emails/smtp-submission-improvements
from django_redis import get_redis_connection
redis_key = "pretix_mail_retry_" + hashlib.sha1(f"{getattr(backend, 'username', '_')}@{getattr(backend, 'host', '_')}".encode()).hexdigest()
rc = get_redis_connection("redis")
cnt = rc.incr(redis_key)
rc.expire(redis_key, 300)
max_retries = 10
retry_after = 30 + cnt * 10
else:
# Most likely some other kind of temporary failure, retry again (but pretty soon)
max_retries = 5
retry_after = 2 ** (self.request.retries * 3) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
try:
self.retry(max_retries=5, countdown=2 ** (self.request.retries * 3)) # max is 2 ** (4*3) = 4096 seconds = 68 minutes
self.retry(max_retries=max_retries, countdown=retry_after)
except MaxRetriesExceededError:
if log_target:
log_target.log_action(
@@ -558,6 +580,10 @@ def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: st
)
logger.exception('Error sending email')
raise SendMailException('Failed to send an email to {}.'.format(to))
else:
for i in invoices_sent:
i.sent_to_customer = now()
i.save(update_fields=['sent_to_customer'])
def mail_send(*args, **kwargs):

View File

@@ -33,6 +33,7 @@ from pretix.base.models import (
CachedFile, Event, InvoiceAddress, Order, OrderPayment, OrderPosition,
User,
)
from pretix.base.models.orders import Transaction
from pretix.base.orderimport import get_all_columns
from pretix.base.services.invoices import generate_invoice, invoice_qualified
from pretix.base.services.tasks import ProfiledEventTask
@@ -146,6 +147,7 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
# quota check?
with event.lock():
with transaction.atomic():
save_transactions = []
for o in orders:
o.total = sum([c.price for c in o._positions]) # currently no support for fees
if o.total == Decimal('0.00'):
@@ -187,6 +189,8 @@ def import_orders(event: Event, fileid: str, settings: dict, locale: str, user)
user=user,
data={'source': 'import'}
)
save_transactions += o.create_transactions(is_new=True, fees=[], positions=o._positions, save=False)
Transaction.objects.bulk_create(save_transactions)
for o in orders:
with language(o.locale, event.settings.region):

View File

@@ -181,6 +181,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
for m in position.granted_memberships.all():
m.canceled = False
m.save()
order.create_transactions()
else:
raise OrderError(is_available)
@@ -202,6 +203,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
if new_date < now():
raise OrderError(_('The new expiry date needs to be in the future.'))
@transaction.atomic
def change(was_expired=True):
order.expires = new_date
if was_expired:
@@ -221,6 +223,7 @@ def extend_order(order: Order, new_date: datetime, force: bool=False, user: User
num_invoices = order.invoices.filter(is_cancellation=False).count()
if num_invoices > 0 and order.invoices.filter(is_cancellation=True).count() >= num_invoices and invoice_qualified(order):
generate_invoice(order)
order.create_transactions()
if order.status == Order.STATUS_PENDING:
change(was_expired=False)
@@ -262,6 +265,7 @@ def mark_order_expired(order, user=None, auth=None):
i = order.invoices.filter(is_cancellation=False).last()
if i and not i.refered.exists():
generate_cancellation(i)
order.create_transactions()
order_expired.send(order.event, order=order)
return order
@@ -280,6 +284,7 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
order.require_approval = False
order.set_expires(now(), order.event.subevents.filter(id__in=[p.subevent_id for p in order.positions.all()]))
order.save(update_fields=['require_approval', 'expires'])
order.create_transactions()
order.log_action('pretix.event.order.approved', user=user, auth=auth)
if order.total == Decimal('0.00'):
@@ -352,6 +357,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
for position in order.positions.all():
if position.voucher:
Voucher.objects.filter(pk=position.voucher.pk).update(redeemed=Greatest(0, F('redeemed') - 1))
order.create_transactions()
order_denied.send(order.event, order=order)
@@ -472,6 +478,8 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
data={'cancellation_fee': cancellation_fee})
order.cancellation_requests.all().delete()
order.create_transactions()
if send_mail:
email_template = order.event.settings.mail_text_order_canceled
with language(order.locale, order.event.settings.region):
@@ -904,7 +912,8 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
fee=pf
)
OrderPosition.transform_cart_positions(positions, order)
orderpositions = OrderPosition.transform_cart_positions(positions, order)
order.create_transactions(positions=orderpositions, fees=fees, is_new=True)
order.log_action('pretix.event.order.placed')
if order.require_approval:
order.log_action('pretix.event.order.placed.require_approval')
@@ -1552,17 +1561,16 @@ class OrderChangeManager:
self.order.save()
elif self.open_payment:
try:
with transaction.atomic():
self.open_payment.payment_provider.cancel_payment(self.open_payment)
self.order.log_action(
'pretix.event.order.payment.canceled',
{
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
},
user=self.user,
auth=self.auth
)
self.open_payment.payment_provider.cancel_payment(self.open_payment)
self.order.log_action(
'pretix.event.order.payment.canceled',
{
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
},
user=self.user,
auth=self.auth
)
except PaymentException as e:
self.order.log_action(
'pretix.event.order.payment.canceled.failed',
@@ -1577,12 +1585,11 @@ class OrderChangeManager:
elif self.order.status in (Order.STATUS_PENDING, Order.STATUS_EXPIRED) and self._totaldiff > 0:
if self.open_payment:
try:
with transaction.atomic():
self.open_payment.payment_provider.cancel_payment(self.open_payment)
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
}, user=self.user, auth=self.auth)
self.open_payment.payment_provider.cancel_payment(self.open_payment)
self.order.log_action('pretix.event.order.payment.canceled', {
'local_id': self.open_payment.local_id,
'provider': self.open_payment.provider,
}, user=self.user, auth=self.auth)
except PaymentException as e:
self.order.log_action(
'pretix.event.order.payment.canceled.failed',
@@ -2122,10 +2129,13 @@ class OrderChangeManager:
self._recalculate_total_and_payment_fee()
if self.order.status in (Order.STATUS_PENDING, Order.STATUS_PAID):
self._reissue_invoice()
self._check_paid_price_change()
self._check_paid_to_free()
self._clear_tickets_cache()
self.order.touch()
self._check_paid_price_change()
self._check_paid_to_free()
self.order.create_transactions()
if self.split_order:
self.split_order.create_transactions()
if self.notify:
notify_user_changed_order(
@@ -2399,6 +2409,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
generate_cancellation(i)
generate_invoice(order)
order.create_transactions()
return old_fee, new_fee, fee, new_payment

View File

@@ -0,0 +1,130 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-2021 rami.io GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import re
from urllib.error import HTTPError
import vat_moss.errors
import vat_moss.id
from django.utils.translation import gettext_lazy as _
from zeep import Client, Transport
from zeep.cache import SqliteCache
from zeep.exceptions import Fault
from pretix.base.models.tax import cc_to_vat_prefix, is_eu_country
logger = logging.getLogger(__name__)
class VATIDError(Exception):
def __init__(self, message):
self.message = message
class VATIDFinalError(VATIDError):
pass
class VATIDTemporaryError(VATIDError):
pass
def _validate_vat_id_EU(vat_id, country_code):
if vat_id[:2] != cc_to_vat_prefix(country_code):
raise VATIDFinalError(_('Your VAT ID does not match the selected country.'))
try:
result = vat_moss.id.validate(vat_id)
if result:
country_code, normalized_id, company_name = result
return normalized_id
except (vat_moss.errors.InvalidError, ValueError):
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
except vat_moss.errors.WebServiceUnavailableError:
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(_(
'Your VAT ID could not be checked, as the VAT checking service of '
'your country is currently not available. We will therefore '
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'
))
except (vat_moss.errors.WebServiceError, HTTPError):
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(_(
'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.'
))
def _validate_vat_id_CH(vat_id, country_code):
if vat_id[:3] != 'CHE':
raise VATIDFinalError(_('Your VAT ID does not match the selected country.'))
vat_id = re.sub('[^A-Z0-9]', '', vat_id.replace('HR', '').replace('MWST', ''))
try:
transport = Transport(cache=SqliteCache())
client = Client(
'https://www.uid-wse-a.admin.ch/V5.0/PublicServices.svc?wsdl',
transport=transport
)
if not client.service.ValidateUID(uid=vat_id):
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
return vat_id
except Fault as e:
if e.message == 'Data_validation_failed':
raise VATIDFinalError(_('This VAT ID is not valid. Please re-check your input.'))
elif e.message == 'Request_limit_exceeded':
logger.exception('VAT ID checking failed for country {} due to request limit'.format(country_code))
raise VATIDTemporaryError(_(
'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:
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(_(
'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.'
))
except:
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(_(
'Your VAT ID could not be checked, as the VAT checking service of '
'your country is currently not available. We will therefore '
'need to charge VAT on your invoice. You can get the tax amount '
'back via the VAT reimbursement process.'
))
def validate_vat_id(vat_id, country_code):
country_code = str(country_code)
if is_eu_country(country_code):
return _validate_vat_id_EU(vat_id, country_code)
elif country_code == 'CH':
return _validate_vat_id_CH(vat_id, country_code)
raise VATIDTemporaryError(f'VAT ID should not be entered for country {country_code}')

View File

@@ -48,10 +48,12 @@ from django.core.validators import (
MaxValueValidator, MinValueValidator, RegexValidator,
)
from django.db.models import Model
from django.utils.functional import lazy
from django.utils.text import format_lazy
from django.utils.translation import (
gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
gettext, gettext_lazy as _, gettext_noop, pgettext, pgettext_lazy,
)
from django_countries.fields import Country
from hierarkey.models import GlobalSettingsBase, Hierarkey
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
from i18nfield.strings import LazyI18nString
@@ -61,7 +63,7 @@ from pretix.api.serializers.fields import (
ListMultipleChoiceField, UploadedFileField,
)
from pretix.api.serializers.i18n import I18nField
from pretix.base.models.tax import TaxRule
from pretix.base.models.tax import VAT_ID_COUNTRIES, TaxRule
from pretix.base.reldate import (
RelativeDateField, RelativeDateTimeField, RelativeDateWrapper,
SerializerRelativeDateField, SerializerRelativeDateTimeField,
@@ -370,7 +372,11 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Ask for VAT ID"),
help_text=_("Does only work if an invoice address is asked for. VAT ID is not required."),
help_text=format_lazy(
_("Only works if an invoice address is asked for. VAT ID is never required and only requested from "
"business customers in the following countries: {countries}"),
countries=lazy(lambda *args: ', '.join(sorted(gettext(Country(cc).name) for cc in VAT_ID_COUNTRIES)), str)()
),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
)
},
@@ -1709,6 +1715,17 @@ Best regards,
Your {event} team"""))
},
'mail_days_order_expire_warning': {
'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField,
'serializer_kwargs': dict(
min_value=0,
),
'form_kwargs': dict(
label=_("Number of days"),
min_value=0,
help_text=_("This email will be sent out this many days before the order expires. If the "
"value is 0, the mail will never be sent.")
),
'type': int,
'default': '3'
},

View File

@@ -134,6 +134,10 @@
height: auto;
}
img {
display: block;
}
.content table {
width: 100%;
}

View File

@@ -0,0 +1,26 @@
{% extends "error.html" %}
{% load i18n %}
{% load rich_text %}
{% load static %}
{% block title %}{% trans "Redirect" %}{% endblock %}
{% block content %}
<i class="fa fa-link fa-fw big-icon"></i>
<div class="error-details">
<h1>{% trans "Redirect" %}</h1>
<h3>
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
{% endblocktrans %}
{% blocktrans trimmed %}
Please only proceed if you trust this website to be safe.
{% endblocktrans %}
</h3>
<p>
<a href="{{ url }}" class="btn btn-primary btn-lg">
{% blocktrans trimmed with host=hostname %}
Proceed to {{ host }}
{% endblocktrans %}
</a>
</p>
</div>
{% endblock %}

View File

@@ -34,6 +34,8 @@ register = template.Library()
def money_filter(value: Decimal, arg='', hide_currency=False):
if isinstance(value, (float, int)):
value = Decimal(value)
if value is None:
value = Decimal('0.00')
if not isinstance(value, Decimal):
if value == '':
return value

View File

@@ -24,6 +24,21 @@ import urllib.parse
from django.core import signing
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.urls import reverse
from django.shortcuts import render
def _is_samesite_referer(request):
referer = request.META.get('HTTP_REFERER')
if referer is None:
return False
referer = urllib.parse.urlparse(referer)
# Make sure we have a valid URL for Referer.
if '' in (referer.scheme, referer.netloc):
return False
return (referer.scheme, referer.netloc) == (request.scheme, request.get_host())
def redir_view(request):
@@ -32,6 +47,14 @@ def redir_view(request):
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
return render(request, 'pretixbase/redirect.html', {
'hostname': u.hostname,
'url': url,
})
r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex'
return r

View File

@@ -607,6 +607,7 @@ class ItemUpdateForm(I18nModelForm):
'issue_giftcard',
'require_membership',
'require_membership_types',
'require_membership_hidden',
'grant_membership_type',
'grant_membership_duration_like_event',
'grant_membership_duration_days',
@@ -713,6 +714,7 @@ class ItemVariationForm(I18nModelForm):
'original_price',
'description',
'require_membership',
'require_membership_hidden',
'require_membership_types',
'available_from',
'available_until',

View File

@@ -57,7 +57,8 @@ from pretix.base.forms.widgets import (
DatePickerWidget, SplitDateTimePickerWidget,
)
from pretix.base.models import (
InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition, TaxRule,
Invoice, InvoiceAddress, ItemAddOn, Order, OrderFee, OrderPosition,
TaxRule,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.pricing import get_price
@@ -609,6 +610,17 @@ class OrderMailForm(forms.Form):
label=_("Subject"),
required=True
)
attach_tickets = forms.BooleanField(
label=_("Attach tickets"),
help_text=_("Will be ignored if all tickets in this order exceed a given size limit to ensure email deliverability."),
required=False
)
attach_invoices = forms.ModelMultipleChoiceField(
label=_("Attach invoices"),
widget=forms.CheckboxSelectMultiple,
required=False,
queryset=Invoice.objects.none()
)
def _set_field_placeholders(self, fn, base_parameters):
phs = [
@@ -641,6 +653,7 @@ class OrderMailForm(forms.Form):
widget=forms.Textarea,
initial=order.event.settings.mail_text_order_custom_mail.localize(order.locale),
)
self.fields['attach_invoices'].queryset = order.invoices.all()
self._set_field_placeholders('message', ['event', 'order'])
@@ -648,6 +661,7 @@ class OrderPositionMailForm(OrderMailForm):
def __init__(self, *args, **kwargs):
position = self.position = kwargs.pop('position')
super().__init__(*args, **kwargs)
del self.fields['attach_invoices']
self.fields['sendto'].initial = position.attendee_email
self.fields['message'] = forms.CharField(
label=_("Message"),

View File

@@ -59,7 +59,7 @@
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
<script type="text/javascript" src="{% static "fileupload/jquery.ui.widget.js" %}"></script>
<script type="text/javascript" src="{% static "fileupload/jquery.fileupload.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.min.js" %}"></script>
<script type="text/javascript" src="{% static "lightbox/js/lightbox.js" %}"></script>
<script type="text/javascript" src="{% static "are-you-sure/jquery.are-you-sure.js" %}"></script>
{% endcompress %}
{{ html_head|safe }}

View File

@@ -74,17 +74,17 @@
{{ c.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if c.type == "exit" %}
{% if c.auto_checked_in %}
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip_html"
<span class="fa fa-fw fa-hourglass-end" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% endif %}
{% elif c.forced and c.successful %}
<span class="fa fa-fw fa-warning" data-toggle="tooltip_html"
<span class="fa fa-fw fa-warning" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}"></span>
{% elif c.forced and not c.successful %}
<br>
<small class="text-muted">{% trans "Failed in offline mode" %}</small>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic" data-toggle="tooltip_html"
<span class="fa fa-fw fa-magic" data-toggle="tooltip"
title="{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}"></span>
{% endif %}
</td>

View File

@@ -1,6 +1,6 @@
{% load i18n %}
<div class="quotabox availability" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}<br>{% blocktrans with date=q.cached_availability_time|date:"SHORT_DATETIME_FORMAT" %}Numbers as of {{ date }}{% endblocktrans %}">
{% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-100">

View File

@@ -1,6 +1,6 @@
{% load i18n %}
<a class="quotabox" data-toggle="tooltip_html" data-placement="top"
title="{% trans "Quota:" %} {{ q.name }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
title="{% trans "Quota:" %} {{ q.name|force_escape|force_escape }}{% if q.cached_avail.1 is not None %}<br>{% blocktrans with num=q.cached_avail.1 %}Currently available: {{ num }}{% endblocktrans %}{% endif %}"
href="{% url "control:event.items.quotas.show" event=q.event.slug organizer=q.event.organizer.slug quota=q.pk %}">
{% if q.size|default_if_none:"NONE" == "NONE" %}
<div class="progress">

View File

@@ -77,6 +77,7 @@
{% bootstrap_field form.require_membership layout="control" %}
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
{% bootstrap_field form.require_membership_types layout="control" %}
{% bootstrap_field form.require_membership_hidden layout="control" %}
</div>
{% endif %}
</div>
@@ -147,6 +148,7 @@
{% bootstrap_field formset.empty_form.require_membership layout="control" %}
<div data-display-dependency="#{{ formset.empty_form.require_membership.id_for_label }}">
{% bootstrap_field formset.empty_form.require_membership_types layout="control" %}
{% bootstrap_field formset.empty_form.require_membership_hidden layout="control" %}
</div>
{% endif %}
</div>

View File

@@ -105,6 +105,7 @@
{% bootstrap_field form.require_membership layout="control" %}
<div data-display-dependency="#{{ form.require_membership.id_for_label }}">
{% bootstrap_field form.require_membership_types layout="control" %}
{% bootstrap_field form.require_membership_hidden layout="control" %}
</div>
{% endif %}
{% bootstrap_field form.allow_cancel layout="control" %}

View File

@@ -24,6 +24,8 @@
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new category" %}
</a>
</p>
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
@@ -33,15 +35,16 @@
<th class="action-col-2"></th>
</tr>
</thead>
<tbody>
<tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
{% for c in categories %}
<tr>
<tr data-dnd-id="{{ c.id }}">
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
</td>
<td>
<a href="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm {% if forloop.counter0 == 0 and is_paginated and not page_obj.has_previous %}disabled{% endif %}"><i class="fa fa-arrow-up"></i></a>
<a href="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm {% if forloop.revcounter0 == 0 and is_paginated and not page_obj.has_next %}disabled{% endif %}"><i class="fa fa-arrow-down"></i></a>
<button formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip">
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
@@ -56,6 +59,7 @@
</tbody>
</table>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}

View File

@@ -26,6 +26,8 @@
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
</p>
<form method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
@@ -36,15 +38,16 @@
<th class="iconcol"></th>
<th class="iconcol"></th>
<th>{% trans "Category" %}</th>
<th class="action-col-2"></th>
<th class="action-col-2"></th>
<th class="action-col-2"><span class="sr-only">Move</span></th>
<th class="action-col-2"><span class="sr-only">Edit</span></th>
</tr>
</thead>
<tbody>
{% regroup items by category as cat_list %}
{% for c in cat_list %}
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
{% for i in c.list %}
<tr {% if not i.active %}class="row-muted"{% endif %}>
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category.name }}</th></tr>{% endif %}
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
<td><strong>
{% if not i.active %}<strike>{% endif %}
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}">{{ i }}</a>
@@ -105,8 +108,9 @@
</td>
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td>
<td>
<a href="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm {% if forloop.counter0 == 0 and is_paginated and not page_obj.has_previous %}disabled{% endif %}"><i class="fa fa-arrow-up"></i></a>
<a href="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm {% if forloop.revcounter0 == 0 and is_paginated and not page_obj.has_next %}disabled{% endif %}"><i class="fa fa-arrow-down"></i></a>
<button formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container"></span>
</td>
<td class="text-right flip col-actions">
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
@@ -115,10 +119,11 @@
</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</tbody>
</table>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}

View File

@@ -52,18 +52,22 @@
{% if order.status == 'n' or order.status == 'e' %}
<a href="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}?status=p"
class="btn {% if overpaid >= 0 %}btn-success{% else %}btn-default{% endif %}">
<span class="fa fa-check"></span>
{% trans "Mark as paid" %}
</a>
<a href="{% url "control:event.order.extend" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
<span class="fa fa-clock-o"></span>
{% trans "Extend payment term" %}
</a>
{% endif %}
{% if order.cancel_allowed %}
<a href="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}?status=c" class="btn btn-default">
<span class="fa fa-ban"></span>
{% trans "Cancel order" %}
</a>
{% elif order.status == 'c' %}
<a href="{% url "control:event.order.reactivate" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
<span class="fa fa-reply"></span>
{% trans "Reactivate order" %}
</a>
{% endif %}
@@ -71,11 +75,17 @@
<a href="{% eventurl request.event "presale:event.order" order=order.code secret=order.secret %}"
class="btn btn-default" target="_blank">
<span class="fa fa-eye"></span>
{% trans "View order as user" %}
</a>
<a href="{% url "control:event.order.mail_history" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
<span class="fa fa-envelope-o"></span>
{% trans "View email history" %}
</a>
<a href="{% url "control:event.order.transactions" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
<span class="fa fa-list"></span>
{% trans "View transaction history" %}
</a>
</div>
</div>
</form>
@@ -234,7 +244,23 @@
{% for i in invoices %}
<a href="{% url "control:event.invoice.download" invoice=i.pk event=request.event.slug organizer=request.event.organizer.slug %}">
{% if i.is_cancellation %}{% trans "Cancellation" context "invoice" %}{% else %}{% trans "Invoice" %}{% endif %}
{{ i.number }}</a> ({{ i.date|date:"SHORT_DATE_FORMAT" }})
{{ i.number }}</a>
({{ i.date|date:"SHORT_DATE_FORMAT" }})
{% if i.sent_to_customer.year == 1970 %}
<span class="fa-stack fa-stack-small" data-toggle="tooltip" title="{% trans "We don't know if this invoice was emailed to the customer since it was created before our system tracked this information" %}">
<span class="fa fa-background fa-envelope text-muted fa-stack-1x"></span>
<span class="fa fa-question fa-stack-1x fa-stack-shifted"></span>
</span>
{% elif i.sent_to_customer %}
<span class="fa-stack fa-stack-small" data-toggle="tooltip" title="{% trans "Invoice was emailed to customer" %}">
<span class="fa fa-background fa-envelope text-muted fa-stack-1x"></span>
<span class="fa fa-check text-success fa-stack-1x fa-stack-shifted"></span>
</span>
{% else %}
<span class="fa-stack fa-stack-small" data-toggle="tooltip" title="{% trans "Invoice was not yet emailed to customer" %}">
<span class="fa fa-background fa-envelope text-muted fa-stack-1x"></span>
</span>
{% endif %}
{% if not i.canceled %}
{% if request.event.settings.invoice_regenerate_allowed %}
<form class="form-inline helper-display-inline" method="post"
@@ -268,6 +294,12 @@
<br/>
{% endif %}
{% endfor %}
{% if invoices_send_link %}
<br/>
<a class="btn btn-default btn-xs" href="{{ invoices_send_link }}">
{% trans "Email invoices" %}
</a>
{% endif %}
{% if can_generate_invoice %}
<br/>
<form class="form-inline helper-display-inline" method="post"
@@ -328,19 +360,19 @@
{% if line.checkins.all %}
{% for c in line.all_checkins.all %}
{% if not c.successful %}
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-exclamation-circle text-danger" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Denied scan: {{ date }}{% endblocktrans %}<br>{{ c.get_error_reason_display }}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.type == "exit" %}
{% if c.auto_checked_in %}
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
<span class="fa fa-fw text-success fa-hourglass-end" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically marked not present: {{ date }}{% endblocktrans %}"></span>
{% else %}
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw text-success fa-sign-out" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Exit scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %}
{% elif c.forced %}
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-warning text-warning" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Additional entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% elif c.auto_checked_in %}
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-magic text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Automatically checked in: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% else %}
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
<span class="fa fa-fw fa-check text-success" data-toggle="tooltip_html" title="{{ c.list.name|force_escape|force_escape }}<br>{% blocktrans trimmed with date=c.datetime|date:'SHORT_DATETIME_FORMAT' %}Entry scan: {{ date }}{% endblocktrans %}{% if c.gate %}<br>{{ c.gate }}{% endif %}"></span>
{% endif %}
{% endfor %}
{% endif %}

View File

@@ -28,7 +28,7 @@
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
{% endif %}
</p>
{% if log.parsed_data.subject.items %}
{% if log.parsed_data.message.items %}
<div class="alert alert-info">
{% blocktrans trimmed %}
This email has been sent with an older version of pretix. We are therefore not able to

View File

@@ -18,6 +18,10 @@
{% bootstrap_field form.sendto layout='horizontal' %}
{% bootstrap_field form.subject layout='horizontal' %}
{% bootstrap_field form.message layout='horizontal' %}
{% bootstrap_field form.attach_tickets layout='horizontal' %}
{% if form.attach_invoices %}
{% bootstrap_field form.attach_invoices layout='horizontal' %}
{% endif %}
{% if request.method == "POST" %}
<fieldset>
<legend>{% trans "E-mail preview" %}</legend>

View File

@@ -0,0 +1,81 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% block title %}{% trans "Transaction history" %}{% endblock %}
{% block content %}
<h1>
{% trans "Transaction history" %}
<a class="btn btn-link btn-lg"
href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
{% blocktrans trimmed with order=order.code %}
Back to order {{ order }}
{% endblocktrans %}
</a>
</h1>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "Product" %}</th>
<th class="text-right flip">{% trans "Tax rate" %}</th>
<th class="text-right flip">{% trans "Quantity" %}</th>
<th class="text-right flip">{% trans "Single price" %}</th>
<th class="text-right flip">{% trans "Total tax value" %}</th>
<th class="text-right flip">{% trans "Total price" %}</th>
</tr>
</thead>
<tbody>
{% for t in transactions %}
<tr class="{% if t.count < 0 %}text-danger{% endif %}">
<td>
{{ t.datetime|date:"SHORT_DATETIME_FORMAT" }}
{% if t.migrated %}
<span class="fa fa-warning text-warning"
data-toggle="tooltip"
title="{% trans 'This order was created before we introduced this table, therefore this data might be inaccurate.' %}"
></span>
{% endif %}
</td>
<td>
{% if t.item %}
{{ t.item }}
{% if t.variation %}
{{ t.variation }}
{% endif %}
{% endif %}
{% if t.fee_type %}
{{ t.get_fee_type_display }}
{% endif %}
{% if t.subevent %}
<br>{{ t.subevent }}
{% endif %}
</td>
<td class="text-right flip">{{ t.tax_rate }} %</td>
<td class="text-right flip">{{ t.count }} &times;</td>
<td class="text-right flip">{{ t.price|money:request.event.currency }}</td>
<td class="text-right flip">{{ t.full_tax_value|money:request.event.currency }}</td>
<td class="text-right flip">{{ t.full_price|money:request.event.currency }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="{% if t.count < 0 %}text-danger{% endif %}">
<td>
<strong>{% trans "Sum" %}</strong>
</td>
<td>
</td>
<td></td>
<td class="text-right flip">
<strong>
{{ sums.count }}
</strong>
</td>
<td></td>
<td class="text-right flip"><strong>{{ sums.full_tax_value|money:request.event.currency }}</strong></td>
<td class="text-right flip"><strong>{{ sums.full_price|money:request.event.currency }}</strong></td>
</tr>
</tfoot>
</table>
{% endblock %}

View File

@@ -239,6 +239,7 @@ urlpatterns = [
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
re_path(r'^items/(?P<item>\d+)/up$', item.item_move_up, name='event.items.up'),
re_path(r'^items/(?P<item>\d+)/down$', item.item_move_down, name='event.items.down'),
re_path(r'^items/reorder$', item.reorder_items, name='event.items.reorder'),
re_path(r'^items/(?P<item>\d+)/delete$', item.ItemDelete.as_view(), name='event.items.delete'),
re_path(r'^items/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),
@@ -250,6 +251,7 @@ urlpatterns = [
re_path(r'^categories/(?P<category>\d+)/up$', item.category_move_up, name='event.items.categories.up'),
re_path(r'^categories/(?P<category>\d+)/down$', item.category_move_down,
name='event.items.categories.down'),
re_path(r'^categories/reorder$', item.reorder_categories, name='event.items.categories.reorder'),
re_path(r'^categories/(?P<category>\d+)/$', item.CategoryUpdate.as_view(),
name='event.items.categories.edit'),
re_path(r'^categories/add$', item.CategoryCreate.as_view(), name='event.items.categories.add'),
@@ -343,6 +345,7 @@ urlpatterns = [
re_path(r'^orders/(?P<code>[0-9A-Z]+)/cancellationrequests/(?P<req>\d+)/delete$',
orders.OrderCancellationRequestDelete.as_view(),
name='event.order.cancellationrequests.delete'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/transactions/$', orders.OrderTransactions.as_view(), name='event.order.transactions'),
re_path(r'^orders/(?P<code>[0-9A-Z]+)/$', orders.OrderDetail.as_view(), name='event.order'),
re_path(r'^invoice/(?P<invoice>[^/]+)$', orders.InvoiceDownload.as_view(),
name='event.invoice.download'),

View File

@@ -53,6 +53,7 @@ from django.urls import resolve, reverse
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import gettext, gettext_lazy as _
from django.views.decorators.http import require_http_methods
from django.views.generic import ListView
from django.views.generic.detail import DetailView, SingleObjectMixin
from django.views.generic.edit import DeleteView
@@ -101,7 +102,8 @@ class ItemList(ListView):
).annotate(
var_count=Count('variations')
).prefetch_related("category").order_by(
'category__position', 'category', 'position'
F('category__position').desc(nulls_first=True),
'category', 'position'
)
def get_context_data(self, **kwargs):
@@ -138,6 +140,7 @@ def item_move(request, item, up=True):
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def item_move_up(request, organizer, event, item):
item_move(request, item, up=True)
return redirect('control:event.items',
@@ -146,6 +149,7 @@ def item_move_up(request, organizer, event, item):
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def item_move_down(request, organizer, event, item):
item_move(request, item, up=False)
return redirect('control:event.items',
@@ -153,6 +157,38 @@ def item_move_down(request, organizer, event, item):
event=request.event.slug)
@transaction.atomic
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def reorder_items(request, organizer, event):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
return HttpResponseBadRequest("expected JSON: {ids:[]}")
input_items = list(request.event.items.filter(id__in=[i for i in ids if i.isdigit()]))
if len(input_items) != len(ids):
raise Http404(_("Some of the provided item ids are invalid."))
item_categories = {i.category_id for i in input_items}
if len(item_categories) > 1:
raise Http404(_("You cannot reorder items spanning different categories."))
# get first and only category
item_category = next(iter(item_categories))
if len(input_items) != request.event.items.filter(category=item_category).count():
raise Http404(_("Not all items have been selected."))
for i in input_items:
pos = ids.index(str(i.pk))
if pos != i.position: # Save unneccessary UPDATE queries
i.position = pos
i.save(update_fields=['position'])
return HttpResponse()
class CategoryDelete(EventPermissionRequiredMixin, DeleteView):
model = ItemCategory
form_class = CategoryForm
@@ -307,6 +343,7 @@ def category_move(request, category, up=True):
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def category_move_up(request, organizer, event, category):
category_move(request, category, up=True)
return redirect('control:event.items.categories',
@@ -315,6 +352,7 @@ def category_move_up(request, organizer, event, category):
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def category_move_down(request, organizer, event, category):
category_move(request, category, up=False)
return redirect('control:event.items.categories',
@@ -322,6 +360,32 @@ def category_move_down(request, organizer, event, category):
event=request.event.slug)
@transaction.atomic
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def reorder_categories(request, organizer, event):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
return HttpResponseBadRequest("expected JSON: {ids:[]}")
input_categories = list(request.event.categories.filter(id__in=[i for i in ids if i.isdigit()]))
if len(input_categories) != len(ids):
raise Http404(_("Some of the provided category ids are invalid."))
if len(input_categories) != request.event.categories.count():
raise Http404(_("Not all categories have been selected."))
for c in input_categories:
pos = ids.index(str(c.pk))
if pos != c.position: # Save unneccessary UPDATE queries
c.position = pos
c.save(update_fields=['position'])
return HttpResponse()
FakeQuestion = namedtuple(
'FakeQuestion', 'id question position required'
)
@@ -423,18 +487,21 @@ class QuestionList(ListView):
@transaction.atomic
@event_permission_required("can_change_items")
@require_http_methods(["POST"])
def reorder_questions(request, organizer, event):
try:
ids = json.loads(request.body.decode('utf-8'))['ids']
except (JSONDecodeError, KeyError, ValueError):
return HttpResponseBadRequest("expected JSON: {ids:[]}")
input_questions = request.event.questions.filter(id__in=[i for i in ids if i.isdigit()])
# filter system_questions - normal questions are int/digit, system_questions strings
custom_question_ids = [i for i in ids if i.isdigit()]
input_questions = list(request.event.questions.filter(id__in=custom_question_ids))
if input_questions.count() != len([i for i in ids if i.isdigit()]):
if len(input_questions) != len(custom_question_ids):
raise Http404(_("Some of the provided question ids are invalid."))
if input_questions.count() != request.event.questions.count():
if len(input_questions) != request.event.questions.count():
raise Http404(_("Not all questions have been selected."))
for q in input_questions:

View File

@@ -42,7 +42,6 @@ from datetime import datetime, time, timedelta
from decimal import Decimal, DecimalException
from urllib.parse import quote, urlencode
import vat_moss.id
from django import forms
from django.conf import settings
from django.contrib import messages
@@ -50,7 +49,7 @@ from django.core.exceptions import ValidationError
from django.core.files import File
from django.db import transaction
from django.db.models import (
Count, Exists, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
Subquery, Sum,
)
from django.forms import formset_factory
@@ -65,7 +64,7 @@ from django.utils.formats import date_format, get_format
from django.utils.functional import cached_property
from django.utils.http import is_safe_url
from django.utils.timezone import make_aware, now
from django.utils.translation import gettext, gettext_lazy as _
from django.utils.translation import gettext, gettext_lazy as _, ngettext
from django.views.generic import (
DetailView, FormView, ListView, TemplateView, View,
)
@@ -83,7 +82,7 @@ from pretix.base.models import (
from pretix.base.models.orders import (
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
)
from pretix.base.models.tax import cc_to_vat_prefix, is_eu_country
from pretix.base.models.tax import ask_for_vat_id
from pretix.base.payment import PaymentException
from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import tickets
@@ -103,6 +102,9 @@ from pretix.base.services.orders import (
notify_user_changed_order, reactivate_order,
)
from pretix.base.services.stats import order_overview
from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
from pretix.base.services.tickets import generate
from pretix.base.signals import (
order_modified, register_data_exporters, register_ticket_outputs,
@@ -313,6 +315,27 @@ class OrderDetail(OrderView):
ctx['download_buttons'] = self.download_buttons
ctx['payment_refund_sum'] = self.order.payment_refund_sum
ctx['pending_sum'] = self.order.pending_sum
unsent_invoices = [ii.pk for ii in ctx['invoices'] if not ii.sent_to_customer]
if unsent_invoices:
ctx['invoices_send_link'] = reverse('control:event.order.sendmail', kwargs={
'event': self.request.event.slug,
'organizer': self.request.event.organizer.slug,
'code': self.order.code
}) + '?' + urlencode({
'subject': ngettext('Your invoice', 'Your invoices', len(unsent_invoices)),
'message': ngettext(
'Hello,\n\nplease find your invoice attached to this email.\n\n'
'Your {event} team',
'Hello,\n\nplease find your invoices attached to this email.\n\n'
'Your {event} team',
len(unsent_invoices)
).format(
event="{event}",
),
'attach_invoices': unsent_invoices
}, doseq=True)
return ctx
@cached_property
@@ -380,6 +403,23 @@ class OrderDetail(OrderView):
}
class OrderTransactions(OrderView):
template_name = 'pretixcontrol/order/transactions.html'
permission = 'can_view_orders'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['transactions'] = self.order.transactions.select_related(
'item', 'variation', 'subevent'
).order_by('datetime')
ctx['sums'] = self.order.transactions.aggregate(
count=Sum('count'),
full_price=Sum(F('count') * F('price')),
full_tax_value=Sum(F('count') * F('tax_value')),
)
return ctx
class OrderDownload(AsyncAction, OrderView):
task = generate
permission = 'can_view_orders'
@@ -1292,26 +1332,18 @@ class OrderCheckVATID(OrderView):
messages.error(self.request, _('No country specified.'))
return redirect(self.get_order_url())
if not is_eu_country(ia.country):
messages.error(self.request, _('VAT ID could not be checked since a non-EU country has been '
'specified.'))
return redirect(self.get_order_url())
if ia.vat_id[:2] != cc_to_vat_prefix(str(ia.country)):
messages.error(self.request, _('Your VAT ID does not match the selected country.'))
if not ask_for_vat_id(ia.country):
messages.error(self.request, _('VAT ID could not be checked since this country is not supported.'))
return redirect(self.get_order_url())
try:
result = vat_moss.id.validate(ia.vat_id)
if result:
country_code, normalized_id, company_name = result
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
except vat_moss.errors.InvalidError:
messages.error(self.request, _('This VAT ID is not valid.'))
except vat_moss.errors.WebServiceUnavailableError:
logger.exception('VAT ID checking failed for country {}'.format(ia.country))
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
except VATIDFinalError as e:
messages.error(self.request, e.message)
except VATIDTemporaryError:
messages.error(self.request, _('The VAT ID could not be checked, as the VAT checking service of '
'the country is currently not available.'))
else:
@@ -1954,6 +1986,8 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
kwargs['initial']['subject'] = self.request.GET.get('subject')
if self.request.GET.get('message'):
kwargs['initial']['message'] = self.request.GET.get('message')
if self.request.GET.getlist('attach_invoices'):
kwargs['initial']['attach_invoices'] = self.order.invoices.filter(pk__in=self.request.GET.getlist('attach_invoices'))
return kwargs
def form_invalid(self, form):
@@ -1982,7 +2016,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
order.send_mail(
form.cleaned_data['subject'], email_template,
email_context, 'pretix.event.order.email.custom_sent',
self.request.user, auto_email=False
self.request.user, auto_email=False,
attach_tickets=form.cleaned_data.get('attach_tickets', False),
invoices=form.cleaned_data.get('attach_invoices', []),
)
messages.success(self.request,
_('Your message has been queued and will be sent to {}.'.format(order.email)))
@@ -2047,7 +2083,8 @@ class OrderPositionSendMail(OrderSendMail):
email_template,
email_context,
'pretix.event.order.position.email.custom_sent',
self.request.user
self.request.user,
attach_tickets=form.cleaned_data.get('attach_tickets', False),
)
messages.success(self.request,
_('Your message has been queued and will be sent to {}.'.format(position.attendee_email)))

View File

@@ -273,6 +273,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
'size': q.size,
'name': q.name,
'release_after_exit': q.release_after_exit,
'ignore_for_event_availability': q.ignore_for_event_availability,
'itemvars': [str(i.pk) for i in q.items.all()] + [
'{}-{}'.format(v.item_id, v.pk) for v in q.variations.all()
]

View File

@@ -33,36 +33,67 @@
# License for the specific language governing permissions and limitations under the License.
from django.template.defaultfilters import date as _date
from django.utils.html import format_html
from django.utils.translation import get_language, gettext_lazy as _
def daterange(df, dt):
def daterange(df, dt, as_html=False):
lng = get_language()
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
if as_html:
base_format = format_html("<time datetime=\"{}\">{{}}</time>", _date(df, "Y-m-d"))
else:
base_format = "{}"
else:
if as_html:
base_format = format_html("<time datetime=\"{}\">{{}}</time>{{}}<time datetime=\"{}\">{{}}</time>", _date(df, "Y-m-d"), _date(dt, "Y-m-d"))
else:
base_format = "{}{}{}"
if lng.startswith("de"):
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
return "{}".format(_date(df, "j. F Y"))
return format_html(base_format, _date(df, "j. F Y"))
elif df.year == dt.year and df.month == dt.month:
return "{}.{}".format(_date(df, "j"), _date(dt, "j. F Y"))
return format_html(base_format, _date(df, "j."), "", _date(dt, "j. F Y"))
elif df.year == dt.year:
return "{} {}".format(_date(df, "j. F"), _date(dt, "j. F Y"))
return format_html(base_format, _date(df, "j. F"), " ", _date(dt, "j. F Y"))
elif lng.startswith("en"):
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
return "{}".format(_date(df, "N jS, Y"))
return format_html(base_format, _date(df, "N jS, Y"))
elif df.year == dt.year and df.month == dt.month:
return "{} {}".format(_date(df, "N jS"), _date(dt, "jS, Y"))
return format_html(base_format, _date(df, "N jS"), " ", _date(dt, "jS, Y"))
elif df.year == dt.year:
return "{} {}".format(_date(df, "N jS"), _date(dt, "N jS, Y"))
return format_html(base_format, _date(df, "N jS"), " ", _date(dt, "N jS, Y"))
elif lng.startswith("es"):
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
return "{}".format(_date(df, "DATE_FORMAT"))
return format_html(base_format, _date(df, "DATE_FORMAT"))
elif df.year == dt.year and df.month == dt.month:
return "{} - {} de {} de {}".format(_date(df, "j"), _date(dt, "j"), _date(dt, "F"), _date(dt, "Y"))
return format_html(
base_format,
_date(df, "j"),
" - ",
"{} de {} de {}".format(_date(dt, "j"), _date(dt, "F"), _date(dt, "Y"))
)
elif df.year == dt.year:
return "{} de {} - {} de {} de {}".format(_date(df, "j"), _date(df, "F"), _date(dt, "j"), _date(dt, "F"), _date(dt, "Y"))
return format_html(
base_format,
"{} de {}".format(_date(df, "j"), _date(df, "F")),
" - ",
"{} de {} de {}".format(_date(dt, "j"), _date(dt, "F"), _date(dt, "Y"))
)
if df.year == dt.year and df.month == dt.month and df.day == dt.day:
return _date(df, "DATE_FORMAT")
return format_html(base_format, _date(df, "DATE_FORMAT"))
if as_html:
base_format = "<time datetime=\"{}\">{}</time>"
return format_html(
"{date_from} {date_to}",
date_from=format_html(base_format, _date(df, "Y-m-d"), _date(df, "DATE_FORMAT")),
date_to=format_html(base_format, _date(dt, "Y-m-d"), _date(dt, "DATE_FORMAT")),
)
return _("{date_from} {date_to}").format(
date_from=_date(df, "DATE_FORMAT"), date_to=_date(dt, "DATE_FORMAT")
date_from=_date(df, "DATE_FORMAT"),
date_to=_date(dt, "DATE_FORMAT"),
)

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -199,6 +199,13 @@ msgstr "تذاكر سارية المفعول"
msgid "Currently inside"
msgstr "حاليا بالداخل"
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "إغلاق"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -261,7 +268,7 @@ msgstr ""
"نعمل الآن على ارسال طلبك إلى الخادم، إذا أستغرقت العملية أكثر من دقيقة، يرجى "
"التحقق من اتصالك بالإنترنت ثم أعد تحميل الصفحة مرة أخرى."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "أغلق الرسالة"
@@ -490,17 +497,25 @@ msgstr[3] "أيام"
msgstr[4] "أيام عديدة"
msgstr[5] "أخرى"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "لم تعد العناصر الموجودة في عربة التسوق الخاصة بك محجوزة لك."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "انتهت صلاحية عربة التسوق"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "سيتم حجز العناصر لك في سلة التسوق لمدة {num}."
msgstr[1] "سيتم حجز العناصر لك في سلة التسوق لمدة دقيقة {num}."
msgstr[2] "سيتم حجز العناصر لك في سلة التسوق لمدة دقيقتين {num}."
@@ -520,20 +535,20 @@ msgstr "ستسترد %(currency)%(amount)"
msgid "Please enter the amount the organizer can keep."
msgstr "الرجاء إدخال المبلغ الذي يمكن للمنظم الاحتفاظ به."
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "الرجاء إدخال عدد لأحد أنواع التذاكر."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "مطلوب"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "المنطقة الزمنية:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "التوقيت المحلي:"

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -194,6 +194,10 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -249,7 +253,7 @@ msgid ""
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr ""
@@ -474,19 +478,25 @@ msgid_plural "({num} more dates)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "El contingut de la cistella ja no el teniu reservat."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "El contingut de la cistella ja no el teniu reservat."
msgstr[1] "El contingut de la cistella ja no el teniu reservat."
#: pretix/static/pretixpresale/js/ui/main.js:144
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -500,22 +510,22 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Cistella expirada"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"PO-Revision-Date: 2021-08-11 05:00+0000\n"
"Last-Translator: Michael <michael.happl@gmx.at>\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-10-18 19:00+0000\n"
"Last-Translator: Tony Pavlik <kontakt@playton.cz>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
"cs/>\n"
"Language: cs\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 4.6\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -194,6 +194,10 @@ msgstr "Platné vstupenky"
msgid "Currently inside"
msgstr "Právě uvnitř"
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr "zavřít"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -261,7 +265,7 @@ msgstr ""
"prosím zkontrolujte své internetové připojení a znovu načtěte stránku a "
"zkuste to znovu."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Zavřít zprávu"
@@ -309,7 +313,7 @@ msgstr "Počet záznamů od půlnoci"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
msgid "Number of days with a previous entry"
msgstr ""
msgstr "Počet dní bez úprav"
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
msgid "All of the conditions below (AND)"
@@ -490,21 +494,25 @@ msgstr[0] "(jeden další termín)"
msgstr[1] "({num} další termíny)"
msgstr[2] "({num} dalších termínů)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
msgstr "Produkty v nákupním košíku již nejsou pro vás rezervovány."
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Produkty v nákupním košíku již nejsou pro vás rezervovány. Pokud je lístek "
"stále dostupný, můžete objednávku dokončit."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Nákupní košík vypršel"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
"Produkty v nákupním košíku jsou pro vás rezervovány po dobu jedné minuty."
"Produkty v nákupním košíku jsou pro vás rezervovány na jednu minutu."
msgstr[1] ""
"Produkty v nákupním košíku jsou pro vás rezervovány na další {num} minuty."
"Produkty v nákupním košíku jsou pro vás rezervovány na {num} minuty."
msgstr[2] ""
"Produkty v nákupním košíku jsou pro vás rezervovány na dalších {num} minut."
@@ -520,20 +528,20 @@ msgstr "Dostanete %(currency)s %(amount)s zpět"
msgid "Please enter the amount the organizer can keep."
msgstr "Zadejte částku, kterou si organizátor může ponechat."
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Zadejte prosím množství pro jeden z typů vstupenek."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "povinný"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Časové pásmo:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Místní čas:"
@@ -675,7 +683,7 @@ msgstr "Uplatnit"
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Voucher code"
msgstr "Kód poukázky"
msgstr "Kód voucheru"
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-09-13 09:48+0000\n"
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -206,6 +206,13 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "Luk"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
#, fuzzy
@@ -283,7 +290,7 @@ msgstr ""
"Din forespørgsel bliver sendt til serveren. Hvis det tager mere end et "
"minut, så tjek din internetforbindelse, genindlæs siden og prøv igen."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Luk besked"
@@ -516,17 +523,25 @@ msgid_plural "({num} more dates)"
msgstr[0] "(en dato mere)"
msgstr[1] "({num} datoer mere)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "Varerne i din kurv er ikke længere reserverede for dig."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Kurv udløbet"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Varerne i din kurv er reserveret for dig i et minut."
msgstr[1] "Varerne i din kurv er reserveret for dig i {num} minutter."
@@ -548,22 +563,22 @@ msgstr "fra %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Kurv udløbet"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Tidszone:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Din lokaltid:"

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"PO-Revision-Date: 2021-08-18 01:00+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
"de/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.6\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -194,6 +194,10 @@ msgstr "Gültige Tickets"
msgid "Currently inside"
msgstr "Derzeit anwesend"
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr "schließen"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -264,7 +268,7 @@ msgstr ""
"dauert, prüfen Sie bitte Ihre Internetverbindung. Danach können Sie diese "
"Seite neu laden und es erneut versuchen."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Schließen"
@@ -495,17 +499,22 @@ msgid_plural "({num} more dates)"
msgstr[0] "(ein weiterer Termin)"
msgstr[1] "({num} weitere Termine)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
msgstr "Die Produkte in Ihrem Warenkorb sind nicht mehr für Sie reserviert."
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Die Produkte in Ihrem Warenkorb sind nicht mehr für Sie reserviert. Sie "
"können die Bestellung trotzdem abschließen, solange die Produkte noch "
"verfügbar sind."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Warenkorb abgelaufen"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
"Die Produkte in Ihrem Warenkorb sind noch eine Minute für Sie reserviert."
msgstr[1] ""
@@ -523,20 +532,20 @@ msgstr "Sie erhalten %(currency)s %(amount)s zurück"
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte geben Sie den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte tragen Sie eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Deine lokale Zeit:"

View File

@@ -60,6 +60,7 @@ Cronjob
csv
Customer
Debug
deliverability
Di
Do
Doe
@@ -77,6 +78,7 @@ Einlassdatum
Einlassuhrzeit
email
Enterprise
EPC
EPS
Erstattungsbetrag
Erstattungsliste
@@ -95,6 +97,7 @@ Explorer
FA
Favicon
Footer
Galicisch
gehostete
geht's
GENEXAMPLE
@@ -102,6 +105,7 @@ Geo
geocoding
gescannt
ggf
GiroCode
giropay
GPL
Guide
@@ -185,6 +189,7 @@ pt
px
QR
Rabattierung
Reader
Rechnungs
Rechungsdatei
Reddit
@@ -257,8 +262,10 @@ txt
ÜBERZAHLT
überzahlte
uhrzeit
UID
ungespeicherte
unkategorisiert
Unkategorisierte
unlöschbar
unlöschbaren
untenstehende

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"PO-Revision-Date: 2021-08-18 01:00+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-10-25 15:40+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix-js/de_Informal/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.6\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -194,6 +194,10 @@ msgstr "Gültige Tickets"
msgid "Currently inside"
msgstr "Derzeit anwesend"
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr "schließen"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -263,7 +267,7 @@ msgstr ""
"dauert, prüfe bitte deine Internetverbindung. Danach kannst du diese Seite "
"neu laden und es erneut versuchen."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Schließen"
@@ -494,17 +498,22 @@ msgid_plural "({num} more dates)"
msgstr[0] "(ein weiterer Termin)"
msgstr[1] "({num} weitere Termine)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
msgstr "Die Produkte in deinem Warenkorb sind nicht mehr für dich reserviert."
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Die Produkte in deinem Warenkorb sind nicht mehr für dich reserviert. Du "
"kannst die Bestellung trotzdem abschließen, solange die Produkte noch "
"verfügbar sind."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Warenkorb abgelaufen"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
"Die Produkte in deinem Warenkorb sind noch eine Minute für dich reserviert."
msgstr[1] ""
@@ -522,20 +531,20 @@ msgstr "Du erhältst %(currency)s %(amount)s zurück"
msgid "Please enter the amount the organizer can keep."
msgstr "Bitte gib den Betrag ein, den der Veranstalter einbehalten darf."
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Bitte trage eine Menge für eines der Produkte ein."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "verpflichtend"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Zeitzone:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Deine lokale Zeit:"

View File

@@ -60,6 +60,7 @@ Cronjob
csv
Customer
Debug
deliverability
Di
Do
Doe
@@ -77,6 +78,7 @@ Einlassdatum
Einlassuhrzeit
email
Enterprise
EPC
EPS
Erstattungsbetrag
Erstattungsliste
@@ -95,6 +97,7 @@ Explorer
FA
Favicon
Footer
Galicisch
gehostete
geht's
GENEXAMPLE
@@ -102,6 +105,7 @@ Geo
geocoding
gescannt
ggf
GiroCode
giropay
GPL
Guide
@@ -185,6 +189,7 @@ pt
px
QR
Rabattierung
Reader
Rechnungs
Rechungsdatei
Reddit
@@ -257,8 +262,10 @@ txt
ÜBERZAHLT
überzahlte
uhrzeit
UID
ungespeicherte
unkategorisiert
Unkategorisierte
unlöschbar
unlöschbaren
untenstehende

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -193,6 +193,10 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -248,7 +252,7 @@ msgid ""
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr ""
@@ -473,17 +477,19 @@ msgid_plural "({num} more dates)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
@@ -499,20 +505,20 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2019-10-03 19:00+0000\n"
"Last-Translator: Chris Spy <chrispiropoulou@hotmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -209,6 +209,13 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "Κλείσιμο"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
#, fuzzy
@@ -290,7 +297,7 @@ msgstr ""
"περισσότερο από ένα λεπτό, ελέγξτε τη σύνδεσή σας στο διαδίκτυο και στη "
"συνέχεια επαναλάβετε τη φόρτωση αυτής της σελίδας και δοκιμάστε ξανά."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Κλείσιμο μηνύματος"
@@ -528,17 +535,25 @@ msgid_plural "({num} more dates)"
msgstr[0] "(μία παραπάνω ημερομηνία)"
msgstr[1] "( {num} περισσότερες ημερομηνίες)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "Τα αντικείμενα στο καλάθι σας δεν είναι πλέον αποκλειστικά για εσάς."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Το καλάθι έληξε"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Τα είδη στο καλάθι θα παραμείνουν δεσμευμένα για ένα λεπτό."
msgstr[1] "Τα είδη στο καλάθι θα παραμείνουν δεσμευμένα για {num} λεπτά."
@@ -560,22 +575,22 @@ msgstr "απο %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Εισαγάγετε μια ποσότητα για έναν από τους τύπους εισιτηρίων."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Το καλάθι έληξε"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-09-06 09:00+0000\n"
"Last-Translator: rauxenz <noquedandireccionesdisponibles@gmail.com>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -194,6 +194,13 @@ msgstr "Tickets válidos"
msgid "Currently inside"
msgstr "Actualmente adentro"
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "Cerrar"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -261,7 +268,7 @@ msgstr ""
"minuto, por favor, revise su conexión a Internet, recargue la página e "
"intente nuevamente."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Cerrar mensaje"
@@ -493,17 +500,25 @@ msgid_plural "({num} more dates)"
msgstr[0] "(una fecha más)"
msgstr[1] "({num} más fechas)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "Los elementos en su carrito de compras ya no se encuentran reservados."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "El carrito de compras ha expirado"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
"Los elementos en su carrito de compras se han reservado por un minuto."
msgstr[1] ""
@@ -521,20 +536,20 @@ msgstr "Obtienes %(currency)s %(price)s de vuelta"
msgid "Please enter the amount the organizer can keep."
msgstr "Por favor, ingrese el monto que el organizador puede quedarse."
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Por favor, introduzca un valor para cada tipo de entrada."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "campo requerido"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Zona horaria:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Su hora local:"

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"PO-Revision-Date: 2021-06-17 00:00+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-10-04 21:00+0000\n"
"Last-Translator: Jaakko Rinta-Filppula <jaakko@r-f.fi>\n"
"Language-Team: Finnish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/fi/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.6\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -176,10 +176,8 @@ msgid "Unknown ticket"
msgstr "Tuntematon lippu"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
#, fuzzy
#| msgid "Ticket not paid"
msgid "Ticket type not allowed here"
msgstr "Lippu maksamatta"
msgstr "Lipputyyppi ei sallittu"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed"
@@ -205,6 +203,13 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "Sulje"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -262,7 +267,7 @@ msgid ""
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Sulje viesti"
@@ -491,19 +496,25 @@ msgid_plural "({num} more dates)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Ostoskori on vanhentunut"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
msgstr[1] "Ostoskorissasi olevat tuotteet eivät ole enää varattu sinulle."
#: pretix/static/pretixpresale/js/ui/main.js:144
msgid "The organizer keeps %(currency)s %(amount)s"
@@ -517,22 +528,22 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Ostoskori on vanhentunut"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Aikavyöhyke:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -6,9 +6,9 @@ msgid ""
msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"PO-Revision-Date: 2020-09-15 17:00+0000\n"
"Last-Translator: Martin Gross <martin@pc-coholic.de>\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-10-01 23:00+0000\n"
"Last-Translator: Fabian Rodriguez <magicfab@legoutdulibre.com>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\n"
"Language: fr\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 3.10.3\n"
"X-Generator: Weblate 4.8\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -73,7 +73,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results"
msgstr ""
msgstr "Résultats de recherche"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
#, fuzzy
@@ -88,7 +88,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
msgid "This ticket requires special attention"
msgstr ""
msgstr "Ce ticket nécessite une attention particulière"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
msgid "Switch direction"
@@ -100,7 +100,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
msgid "Exit"
msgstr ""
msgstr "Quitter"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
msgid "Scan a ticket or search and press return…"
@@ -121,7 +121,7 @@ msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
msgid "Canceled"
msgstr ""
msgstr "Annulé"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
#, fuzzy
@@ -132,7 +132,7 @@ msgstr "Echanger"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Cancel"
msgstr ""
msgstr "Annuler"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
#, fuzzy
@@ -144,7 +144,7 @@ msgstr "Continuer"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Ticket not paid"
msgstr ""
msgstr "Ticket non payé"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "This ticket is not yet paid. Do you want to continue anyways?"
@@ -206,6 +206,13 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "Fermer"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
#, fuzzy
@@ -283,7 +290,7 @@ msgstr ""
"d'une minute, veuillez vérifier votre connexion Internet, puis recharger "
"cette page et réessayer."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Fermer le message"
@@ -410,7 +417,7 @@ msgstr "Zone de code-barres"
#: pretix/static/pretixcontrol/js/ui/editor.js:533
msgid "Powered by pretix"
msgstr "Généré par pretix"
msgstr "Propulsé par pretix"
#: pretix/static/pretixcontrol/js/ui/editor.js:535
msgid "Object"
@@ -449,8 +456,7 @@ msgstr "Erreur inconnue."
#: pretix/static/pretixcontrol/js/ui/main.js:271
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
"Votre choix de couleur est très facile à lire, il a un excellent contraste !"
msgstr "Votre choix couleur a un bon contraste et il est très facile à lire!"
#: pretix/static/pretixcontrol/js/ui/main.js:275
msgid "Your color has decent contrast and is probably good-enough to read!"
@@ -521,17 +527,25 @@ msgid_plural "({num} more dates)"
msgstr[0] "(une date en plus)"
msgstr[1] "({num} plus de dates)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "Les articles de votre panier ne vous sont plus réservés."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Panier expiré"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Les articles de votre panier vous sont réservés pour une minute."
msgstr[1] "Les articles de votre panier vous sont réservés pour {num} minutes."
@@ -553,22 +567,22 @@ msgstr "de %(currency)s %(price)s"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "SVP entrez une quantité pour un de vos types de billets."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "Panier expiré"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,792 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:68
msgid "Marked as paid"
msgstr ""
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:76
msgid "Comment:"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Placed orders"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:15
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Paid orders"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
msgid "Total revenue"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:12
msgid "Contacting Stripe …"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:60
msgid "Total"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:152
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:183
msgid "Confirming your payment …"
msgstr ""
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js:159
msgid "Contacting your bank …"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:30
msgid "Select a check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:31
msgid "No active check-in lists found."
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:32
msgid "Switch check-in list"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:33
msgid "Search results"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:34
msgid "No tickets found"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:35
msgid "Result"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:36
msgid "This ticket requires special attention"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:37
msgid "Switch direction"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:38
msgid "Entry"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:39
msgid "Exit"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:40
msgid "Scan a ticket or search and press return…"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:41
msgid "Load more"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:42
msgid "Valid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:43
msgid "Unpaid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:44
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:45
msgid "Canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
msgid "Redeemed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
msgid "Cancel"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
msgid "Continue"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
msgid "Ticket not paid"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
msgid "This ticket is not yet paid. Do you want to continue anyways?"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
msgid "Additional information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
msgid "Valid ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
msgid "Exit recorded"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
msgid "Ticket already used"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
msgid "Information required"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
msgid "Unknown ticket"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
msgid "Ticket type not allowed here"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
msgid "Entry not allowed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
msgid "Ticket code revoked/changed"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
msgid "Order canceled"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
msgid "Checked-in Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
msgid "Valid Tickets"
msgstr ""
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
"Your request is currently being processed. Depending on the size of your "
"event, this might take up to a few minutes."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:48
#: pretix/static/pretixbase/js/asynctask.js:124
msgid "Your request has been queued on the server and will soon be processed."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:54
#: pretix/static/pretixbase/js/asynctask.js:130
msgid ""
"Your request arrived on the server but we still wait for it to be processed. "
"If this takes longer than two minutes, please contact us or go back in your "
"browser and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:89
#: pretix/static/pretixbase/js/asynctask.js:175
#: pretix/static/pretixbase/js/asynctask.js:180
#: pretix/static/pretixcontrol/js/ui/mail.js:24
msgid "An error of type {code} occurred."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:92
msgid ""
"We currently cannot reach the server, but we keep trying. Last error code: "
"{code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:144
#: pretix/static/pretixcontrol/js/ui/mail.js:21
msgid "The request took too long. Please try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:183
#: pretix/static/pretixcontrol/js/ui/mail.js:26
msgid ""
"We currently cannot reach the server. Please try again. Error code: {code}"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:205
msgid "We are processing your request …"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:213
msgid ""
"We are currently sending your request to the server. If this takes longer "
"than one minute, please check your internet connection and then reload this "
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr ""
#: pretix/static/pretixcontrol/js/clipboard.js:23
msgid "Copied!"
msgstr ""
#: pretix/static/pretixcontrol/js/clipboard.js:29
msgid "Press Ctrl-C to copy!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:10
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:16
msgid "is one of"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:22
msgid "is before"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:26
msgid "is after"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:59
msgid "Product"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:63
msgid "Product variation"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:67
msgid "Current date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:71
msgid "Number of previous entries"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:75
msgid "Number of previous entries since midnight"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:79
msgid "Number of days with a previous entry"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:97
msgid "All of the conditions below (AND)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:98
msgid "At least one of the conditions below (OR)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
msgid "Event start"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:100
msgid "Event end"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:101
msgid "Event admission"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:102
msgid "custom date and time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
msgid "custom time"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:104
msgid "Tolerance (minutes)"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:105
msgid "Add condition"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:106
msgid "minutes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:69
msgid "Lead Scan QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:71
msgid "Check-in QR"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:313
msgid "The PDF background file could not be loaded for the following reason:"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:521
msgid "Group of objects"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:527
msgid "Text object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:529
msgid "Barcode area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:531
msgid "Image area"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:533
msgid "Powered by pretix"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:535
msgid "Object"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:539
msgid "Ticket design"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:813
msgid "Saving failed."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:862
#: pretix/static/pretixcontrol/js/ui/editor.js:901
msgid "Error while uploading your PDF file, please try again."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/editor.js:886
msgid "Do you really want to leave the editor without saving your changes?"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:19
msgid "An error has occurred."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/mail.js:54
msgid "Generating messages …"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:70
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:271
msgid "Your color has great contrast and is very easy to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:275
msgid "Your color has decent contrast and is probably good-enough to read!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:279
msgid ""
"Your color has bad contrast for text on white background, please choose a "
"darker shade."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:417
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:418
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:419
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:422
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:834
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:870
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:911
msgid "You have unsaved changes!"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/orderchange.js:25
msgid "Calculating default price…"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:42
msgid "Others"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:82
msgid "Count"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:136
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "Yes"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/question.js:137
#: pretix/static/pretixpresale/js/ui/questions.js:269
msgid "No"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/subevent.js:111
msgid "(one more date)"
msgid_plural "({num} more dates)"
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
#: pretix/static/pretixpresale/js/ui/main.js:144
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:152
msgid "You get %(currency)s %(amount)s back"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:168
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Sold out"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Buy"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Register"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Reserved"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "FREE"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:22
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:23
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:25
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:26
msgctxt "widget"
msgid "plus taxes"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:27
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:29
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid ""
"There are currently a lot of users in this ticket shop. Please open the shop "
"in a new tab to continue."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "Open ticket shop"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "The cart could not be created. Please try again later"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid ""
"We could not create your cart, since there are currently too many users in "
"this ticket shop. Please click \"Continue\" to retry in a new tab."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:38
msgctxt "widget"
msgid "Waiting list"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid ""
"You currently have an active cart for this event. If you select more "
"products, they will be added to your existing cart."
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Resume checkout"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Redeem a voucher"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Redeem"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:44
msgctxt "widget"
msgid "Voucher code"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Close"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "Continue"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"
msgid "See variations"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:48
msgctxt "widget"
msgid "Choose a different event"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:49
msgctxt "widget"
msgid "Choose a different date"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:50
msgctxt "widget"
msgid "Back"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:51
msgctxt "widget"
msgid "Next month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:52
msgctxt "widget"
msgid "Previous month"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:53
msgctxt "widget"
msgid "Next week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:54
msgctxt "widget"
msgid "Previous week"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:55
msgctxt "widget"
msgid "Open seat selection"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:56
msgctxt "widget"
msgid "Load more"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:58
msgid "Mo"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:59
msgid "Tu"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:60
msgid "We"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:61
msgid "Th"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:62
msgid "Fr"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:63
msgid "Sa"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:64
msgid "Su"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:67
msgid "January"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:68
msgid "February"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:69
msgid "March"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:70
msgid "April"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:71
msgid "May"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:72
msgid "June"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:73
msgid "July"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:74
msgid "August"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:75
msgid "September"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:76
msgid "October"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:77
msgid "November"
msgstr ""
#: pretix/static/pretixpresale/js/widget/widget.js:78
msgid "December"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-09-24 13:54+0000\n"
"Last-Translator: ofirtro <ofir.tro@gmail.com>\n"
"Language-Team: Hebrew <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -195,6 +195,10 @@ msgstr "כרטיסים תקפים"
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
msgid "close"
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -252,7 +256,7 @@ msgid ""
"page and try again."
msgstr ""
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr ""
@@ -479,17 +483,19 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr ""
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
@@ -507,20 +513,20 @@ msgstr ""
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2020-01-24 08:00+0000\n"
"Last-Translator: Prokaj Miklós <mixolid0@gmail.com>\n"
"Language-Team: Hungarian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -207,6 +207,13 @@ msgstr ""
msgid "Currently inside"
msgstr ""
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "Bezárás"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
#, fuzzy
@@ -283,7 +290,7 @@ msgstr ""
"hosszabb időt vesz igénybe, kérjük ellenőrizze az internetkapcsolatát, "
"frissítse az oldalt és próbálkozzon újra."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Üzenet bezárása"
@@ -516,17 +523,25 @@ msgid_plural "({num} more dates)"
msgstr[0] "(még egy időpont)"
msgstr[1] "(még {num} időpont)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "A kosárba helyezett termékek tovább nincsenek tovább foglalva."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "A kosár tartalma egy percig foglalva van számodra."
msgstr[1] "A kosár tartalma {num} percig foglalva van számodra."
@@ -548,22 +563,22 @@ msgstr "%(currency) %(price)-tól"
msgid "Please enter the amount the organizer can keep."
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Adjon meg egy mennyiséget az egyik jegytípusból."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
#, fuzzy
#| msgid "Cart expired"
msgid "required"
msgstr "A kosár lejárt"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr ""
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr ""

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-09-27 18:49+0000\n"
"POT-Creation-Date: 2021-10-29 10:10+0000\n"
"PO-Revision-Date: 2021-07-19 11:57+0000\n"
"Last-Translator: dedecosta <dedecosta2@live.it>\n"
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -194,6 +194,13 @@ msgstr "Biglietti validi"
msgid "Currently inside"
msgstr "Attualmente all'interno"
#: pretix/static/lightbox/js/lightbox.js:96
#, fuzzy
#| msgctxt "widget"
#| msgid "Close"
msgid "close"
msgstr "Chiudi"
#: pretix/static/pretixbase/js/asynctask.js:43
#: pretix/static/pretixbase/js/asynctask.js:119
msgid ""
@@ -261,7 +268,7 @@ msgstr ""
"più di un minuto si prega di verificare la connessione internet e ricaricare "
"la pagina per riprovare l'invio."
#: pretix/static/pretixbase/js/asynctask.js:262
#: pretix/static/pretixbase/js/asynctask.js:264
#: pretix/static/pretixcontrol/js/ui/main.js:34
msgid "Close message"
msgstr "Messaggio di chiusura"
@@ -488,17 +495,25 @@ msgid_plural "({num} more dates)"
msgstr[0] "(un'altra data)"
msgstr[1] "({num} altre date)"
#: pretix/static/pretixpresale/js/ui/cart.js:39
msgid "The items in your cart are no longer reserved for you."
#: pretix/static/pretixpresale/js/ui/cart.js:43
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "I prodotti nel tuo carrello non sono più disponibili."
#: pretix/static/pretixpresale/js/ui/cart.js:41
#: pretix/static/pretixpresale/js/ui/cart.js:45
msgid "Cart expired"
msgstr "Carrello scaduto"
#: pretix/static/pretixpresale/js/ui/cart.js:46
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
#: pretix/static/pretixpresale/js/ui/cart.js:50
#, fuzzy
#| msgid "The items in your cart are reserved for you for one minute."
#| msgid_plural ""
#| "The items in your cart are reserved for you for {num} minutes."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "Gli elementi nel tuo carrello sono riservati per 1 minuto."
msgstr[1] "Gli elementi nel tuo carrello sono riservati per {num} minuti."
@@ -514,20 +529,20 @@ msgstr "Ricevi indietro %(currency)s %(amount)s"
msgid "Please enter the amount the organizer can keep."
msgstr "Inserisci l'importo che l'organizzatore può trattenere."
#: pretix/static/pretixpresale/js/ui/main.js:309
#: pretix/static/pretixpresale/js/ui/main.js:335
msgid "Please enter a quantity for one of the ticket types."
msgstr "Inserisci la quantità per una tipologia di biglietto."
#: pretix/static/pretixpresale/js/ui/main.js:345
#: pretix/static/pretixpresale/js/ui/main.js:371
msgid "required"
msgstr "richiesto"
#: pretix/static/pretixpresale/js/ui/main.js:447
#: pretix/static/pretixpresale/js/ui/main.js:465
#: pretix/static/pretixpresale/js/ui/main.js:473
#: pretix/static/pretixpresale/js/ui/main.js:491
msgid "Time zone:"
msgstr "Fuso orario:"
#: pretix/static/pretixpresale/js/ui/main.js:456
#: pretix/static/pretixpresale/js/ui/main.js:482
msgid "Your local time:"
msgstr "Ora locale:"

File diff suppressed because it is too large Load Diff

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