forked from CGM_Public/pretix_original
Compare commits
239
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c014a91b0 | ||
|
|
9fe6916ab5 | ||
|
|
634263f1ba | ||
|
|
67265e94a0 | ||
|
|
0fa2e9b5dd | ||
|
|
c99d93a078 | ||
|
|
9e20fac0da | ||
|
|
3e4ccc53be | ||
|
|
ce88dfa530 | ||
|
|
f0a06cd9fe | ||
|
|
7672e6274d | ||
|
|
061f578b29 | ||
|
|
79f8501a09 | ||
|
|
c5237b5021 | ||
|
|
0d6f7e74a3 | ||
|
|
21bd4a86a7 | ||
|
|
750f641018 | ||
|
|
2a385d14c4 | ||
|
|
6a7ab1bdf5 | ||
|
|
a73c4ad937 | ||
|
|
043e2eb9cf | ||
|
|
c0fb93ea3b | ||
|
|
4f9297e7d8 | ||
|
|
70b48fdd4b | ||
|
|
e7b5317431 | ||
|
|
63ef2e70e2 | ||
|
|
c4db2a48b6 | ||
|
|
de255b021e | ||
|
|
d3fce71b7f | ||
|
|
37dea068ce | ||
|
|
b5ef49cd3c | ||
|
|
245c5972c6 | ||
|
|
6597977752 | ||
|
|
580137577e | ||
|
|
a09550ce02 | ||
|
|
1b444b780d | ||
|
|
f41d8bb761 | ||
|
|
365dbe7a14 | ||
|
|
bf6078efb6 | ||
|
|
695b9a2ed6 | ||
|
|
25a15069ed | ||
|
|
11a4ea7b77 | ||
|
|
4e5e7df201 | ||
|
|
8f515aa327 | ||
|
|
dbc7fda2f8 | ||
|
|
5e686674ae | ||
|
|
5660cd7f93 | ||
|
|
a7a33ed165 | ||
|
|
9ffdf979f4 | ||
|
|
7338381e58 | ||
|
|
ce06672334 | ||
|
|
223f095611 | ||
|
|
b625dc9ec8 | ||
|
|
50c4a1c376 | ||
|
|
6fd2e42426 | ||
|
|
da651df4f0 | ||
|
|
a6f527e32d | ||
|
|
3ea61fbd1f | ||
|
|
6a959d4220 | ||
|
|
92959dbb1f | ||
|
|
6d6f3c4af8 | ||
|
|
8d14a285ca | ||
|
|
a6b8cd8a54 | ||
|
|
cb95cdc6ce | ||
|
|
cc7b00e206 | ||
|
|
136c54b9a8 | ||
|
|
28ba434e45 | ||
|
|
09320093ad | ||
|
|
89cfab6cad | ||
|
|
a180ce4c51 | ||
|
|
1200274ebf | ||
|
|
877401d8c0 | ||
|
|
44170c1b93 | ||
|
|
ce34bd0a13 | ||
|
|
b58f05efd0 | ||
|
|
3ac70e6e3a | ||
|
|
90123d6a58 | ||
|
|
48a3984db6 | ||
|
|
fe6ee4437f | ||
|
|
f470389cd8 | ||
|
|
c848594c21 | ||
|
|
e9a95b0b09 | ||
|
|
3b48b0782d | ||
|
|
b55bd8f75a | ||
|
|
39caadb335 | ||
|
|
dd6ebd7a48 | ||
|
|
ab576bb643 | ||
|
|
8bc16af36e | ||
|
|
537044bdc8 | ||
|
|
1ac7d03bb8 | ||
|
|
b939fad1c0 | ||
|
|
fb3a608c54 | ||
|
|
ebda10542e | ||
|
|
93dd6bf34d | ||
|
|
52148ebb7a | ||
|
|
82b4fe2733 | ||
|
|
79750e4f4b | ||
|
|
dbabbf7aab | ||
|
|
6158a1f2a4 | ||
|
|
68f6f921b5 | ||
|
|
d0e672435a | ||
|
|
5bc622bcfe | ||
|
|
d0184c1f48 | ||
|
|
e28bbb7ea0 | ||
|
|
fe54a42fc7 | ||
|
|
7365f165ad | ||
|
|
90ce802a33 | ||
|
|
d463878514 | ||
|
|
dd7ee84d29 | ||
|
|
f1e2d1f44c | ||
|
|
87a6a58f32 | ||
|
|
b2dd56bd41 | ||
|
|
f0ceab2305 | ||
|
|
14e3316dd9 | ||
|
|
eb28fdcba9 | ||
|
|
49e4a0faa0 | ||
|
|
e486089590 | ||
|
|
4bb02d4ad9 | ||
|
|
8ad852d9cb | ||
|
|
868fcfc471 | ||
|
|
d847c9a095 | ||
|
|
d1c6b22624 | ||
|
|
842987f48e | ||
|
|
58fc13ed91 | ||
|
|
8010d2e6bb | ||
|
|
1566f54764 | ||
|
|
9d380557e1 | ||
|
|
5758e0dd68 | ||
|
|
b4629e24a5 | ||
|
|
27f5121211 | ||
|
|
a5007e4bd6 | ||
|
|
fb3046210b | ||
|
|
37908bd042 | ||
|
|
74bcbe8f07 | ||
|
|
29f378c58b | ||
|
|
9b537aeb5c | ||
|
|
4b5cd35a0e | ||
|
|
e0675233d5 | ||
|
|
e9726a5227 | ||
|
|
78b65d0757 | ||
|
|
ef620ceb37 | ||
|
|
8575a5f1cd | ||
|
|
5762ffc035 | ||
|
|
79286bb051 | ||
|
|
05a2f411db | ||
|
|
e93e5c047c | ||
|
|
2619a658c9 | ||
|
|
808775c76b | ||
|
|
9f297fbd25 | ||
|
|
d882da0adb | ||
|
|
73bd4a746e | ||
|
|
bc5d0763f3 | ||
|
|
ff084f04b1 | ||
|
|
71af40a08b | ||
|
|
ef60093bae | ||
|
|
49c4cc639f | ||
|
|
e2800019f6 | ||
|
|
c44ea8aa81 | ||
|
|
47a03e1b2a | ||
|
|
86ddca15ca | ||
|
|
294b3966b0 | ||
|
|
fecc00231b | ||
|
|
b3dfc459f5 | ||
|
|
e21d63a7be | ||
|
|
9a807df158 | ||
|
|
e95d551711 | ||
|
|
7188e44fe5 | ||
|
|
a6a93555b6 | ||
|
|
94eb473e42 | ||
|
|
cbc3a344c1 | ||
|
|
47db52d75f | ||
|
|
ba99fe597c | ||
|
|
b638c00952 | ||
|
|
bfcca7046a | ||
|
|
ad5d10ff67 | ||
|
|
54d327deea | ||
|
|
d6505f946f | ||
|
|
9c4efa7dcf | ||
|
|
e6d26c4962 | ||
|
|
7ddbbe21f7 | ||
|
|
8d5ad0bd9e | ||
|
|
aff6a6f022 | ||
|
|
46008818ce | ||
|
|
95db04bad2 | ||
|
|
d0c62ec1cf | ||
|
|
d6cbb130bd | ||
|
|
097d2fcda0 | ||
|
|
41a7c13970 | ||
|
|
1b725810dd | ||
|
|
251f486480 | ||
|
|
a7afcdf753 | ||
|
|
0722341073 | ||
|
|
207bf101b8 | ||
|
|
e8f7cea1bf | ||
|
|
aa55eb2de2 | ||
|
|
9dc5c1b266 | ||
|
|
514f1def4d | ||
|
|
c2bc97a0d8 | ||
|
|
7fba473426 | ||
|
|
be87ba0000 | ||
|
|
76b7643c39 | ||
|
|
b1a3963b33 | ||
|
|
586e694ff3 | ||
|
|
f4383c67a4 | ||
|
|
46b2214836 | ||
|
|
0e20d897d2 | ||
|
|
0c09cccd4f | ||
|
|
5ca0833db1 | ||
|
|
7a63498333 | ||
|
|
b8c0887f79 | ||
|
|
9da65f60d7 | ||
|
|
806124304a | ||
|
|
0d57673a47 | ||
|
|
166b5e4f3b | ||
|
|
541b8f5bd6 | ||
|
|
d2b96b2425 | ||
|
|
04d4c4f8f1 | ||
|
|
f4da94cbcd | ||
|
|
97e3d5387f | ||
|
|
8fc07523a9 | ||
|
|
f18b0ae187 | ||
|
|
f7e16f56ac | ||
|
|
3f4e869cea | ||
|
|
8c2a1d58f4 | ||
|
|
0b05eb34f4 | ||
|
|
be48c5f94c | ||
|
|
cebb6d3b43 | ||
|
|
0de96ed066 | ||
|
|
a9d506b1fa | ||
|
|
7a01057429 | ||
|
|
64e1a602d6 | ||
|
|
fe060c387a | ||
|
|
1dba4c7cc9 | ||
|
|
20b2a3d2aa | ||
|
|
044f0c5480 | ||
|
|
4d394f9e8a | ||
|
|
247c4c6c9c | ||
|
|
11a038feb3 | ||
|
|
9d57ea8534 |
+16
-16
@@ -1,29 +1,30 @@
|
|||||||
before_script:
|
|
||||||
tests:
|
tests:
|
||||||
|
image:
|
||||||
|
name: pretix/ci-image
|
||||||
stage: test
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- pip install -U pip uv
|
||||||
|
- uv pip install --system -U wheel setuptools
|
||||||
script:
|
script:
|
||||||
- virtualenv env
|
- uv pip install --system -e ".[dev]"
|
||||||
- source env/bin/activate
|
|
||||||
- pip install -U pip wheel setuptools
|
|
||||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
|
||||||
- cd src
|
- cd src
|
||||||
- python manage.py check
|
- python manage.py check
|
||||||
- make all compress
|
- make all compress
|
||||||
- py.test --reruns 3 -n 3 tests
|
- PRETIX_CONFIG_FILE=tests/travis_sqlite.cfg py.test --reruns 3 -n 3 tests --maxfail=100
|
||||||
tags:
|
|
||||||
- python3
|
|
||||||
except:
|
except:
|
||||||
- pypi
|
- pypi
|
||||||
pypi:
|
pypi:
|
||||||
stage: release
|
stage: release
|
||||||
|
image:
|
||||||
|
name: pretix/ci-image
|
||||||
|
before_script:
|
||||||
|
- cat $PYPIRC > ~/.pypirc
|
||||||
|
- pip install -U pip uv
|
||||||
|
- uv pip install --system -U wheel setuptools twine build pretix-plugin-build check-manifest
|
||||||
script:
|
script:
|
||||||
- cp /keys/.pypirc ~/.pypirc
|
- uv pip install --system -e ".[dev]"
|
||||||
- virtualenv env
|
|
||||||
- source env/bin/activate
|
|
||||||
- pip install -U pip wheel setuptools check-manifest twine
|
|
||||||
- XDG_CACHE_HOME=/cache pip3 install -e ".[dev]"
|
|
||||||
- python setup.py sdist
|
- python setup.py sdist
|
||||||
- pip install dist/pretix-*.tar.gz
|
- uv pip install --system dist/pretix-*.tar.gz
|
||||||
- python -m pretix migrate
|
- python -m pretix migrate
|
||||||
- python -m pretix check
|
- python -m pretix check
|
||||||
- cd src
|
- cd src
|
||||||
@@ -33,13 +34,12 @@ pypi:
|
|||||||
- python -m build
|
- python -m build
|
||||||
- twine check dist/*
|
- twine check dist/*
|
||||||
- twine upload dist/*
|
- twine upload dist/*
|
||||||
tags:
|
|
||||||
- python3
|
|
||||||
only:
|
only:
|
||||||
- pypi
|
- pypi
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- src/dist/
|
- src/dist/
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ if [ "$1" == "taskworker" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$1" == "upgrade" ]; then
|
if [ "$1" == "upgrade" ]; then
|
||||||
exec python3 -m pretix updatestyles
|
exec python3 -m pretix updateassets
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec python3 -m pretix "$@"
|
exec python3 -m pretix "$@"
|
||||||
|
|||||||
@@ -3,11 +3,16 @@
|
|||||||
.. _`community`:
|
.. _`community`:
|
||||||
|
|
||||||
Community install guides
|
Community install guides
|
||||||
================================
|
========================
|
||||||
|
|
||||||
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
|
.. warning:: The guides are maintained by the community and not by the pretix core team. If you encounter any issues with the guides, please report them to the maintainers of the guides. The pretix core team can not provide support for installs using these guides.
|
||||||
|
|
||||||
Kubernetes
|
Kubernetes
|
||||||
------------
|
----------
|
||||||
|
|
||||||
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.
|
- Helm Chart by techwolf12 - A Helm chart for deploying pretix on Kubernetes. The chart documentation is available on `ArtifactHub <https://artifacthub.io/packages/helm/techwolf12/pretix>`_ and the source code is available on `GitHub <https://github.com/Techwolf12/charts/tree/main/pretix-helm>`_.
|
||||||
|
|
||||||
|
Docker
|
||||||
|
------
|
||||||
|
|
||||||
|
- `docker compose setup <https://github.com/ZPascal/pretix-docker-compose>`_ by ZPascal
|
||||||
@@ -19,7 +19,7 @@ You can use ``pip`` to update pretix directly to the development branch. Then, u
|
|||||||
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
|
(venv)$ pip3 install -U "git+https://github.com/pretix/pretix.git#egg=pretix"
|
||||||
(venv)$ python -m pretix migrate
|
(venv)$ python -m pretix migrate
|
||||||
(venv)$ python -m pretix rebuild
|
(venv)$ python -m pretix rebuild
|
||||||
(venv)$ python -m pretix updatestyles
|
(venv)$ python -m pretix updateassets
|
||||||
# systemctl restart pretix-web pretix-worker
|
# systemctl restart pretix-web pretix-worker
|
||||||
|
|
||||||
Docker installation
|
Docker installation
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ To upgrade to a new pretix release, pull the latest code changes and run the fol
|
|||||||
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
|
(venv)$ pip3 install -U --upgrade-strategy eager pretix gunicorn
|
||||||
(venv)$ python -m pretix migrate
|
(venv)$ python -m pretix migrate
|
||||||
(venv)$ python -m pretix rebuild
|
(venv)$ python -m pretix rebuild
|
||||||
(venv)$ python -m pretix updatestyles
|
(venv)$ python -m pretix updateassets
|
||||||
# systemctl restart pretix-web pretix-worker
|
# 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. Pay special
|
Make sure to also read :ref:`update_notes` and the release notes of the version you are updating to. Pay special
|
||||||
@@ -325,7 +325,7 @@ Then, proceed like after any plugin installation::
|
|||||||
|
|
||||||
(venv)$ python -m pretix migrate
|
(venv)$ python -m pretix migrate
|
||||||
(venv)$ python -m pretix rebuild
|
(venv)$ python -m pretix rebuild
|
||||||
(venv)$ python -m pretix updatestyles
|
(venv)$ python -m pretix updateassets
|
||||||
# systemctl restart pretix-web pretix-worker
|
# systemctl restart pretix-web pretix-worker
|
||||||
|
|
||||||
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
|
.. _Postfix: https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-as-a-send-only-smtp-server-on-ubuntu-22-04
|
||||||
|
|||||||
@@ -103,10 +103,10 @@ pretix_celery_tasks_queued_count
|
|||||||
pretix_celery_tasks_queued_age_seconds
|
pretix_celery_tasks_queued_age_seconds
|
||||||
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
The age of the longest-waiting in the worker queue in seconds, labeled with ``queue``.
|
||||||
|
|
||||||
pretix_successful_logins
|
pretix_logins_successful
|
||||||
Counter. The number of successful backend logins.
|
Counter. The number of successful backend logins.
|
||||||
|
|
||||||
pretix_failed_logins
|
pretix_logins_failed
|
||||||
Counter. The number of failed backend logins, labeled with ``reason``.
|
Counter. The number of failed backend logins, labeled with ``reason``.
|
||||||
|
|
||||||
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
.. _metric types: https://prometheus.io/docs/concepts/metric_types/
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Frontend
|
|||||||
--------
|
--------
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, sass_preamble, sass_postamble, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
:members: html_head, html_footer, footer_link, global_footer_link, front_page_top, front_page_bottom, front_page_bottom_widget, fee_calculation_for_cart, contact_form_fields, question_form_fields, contact_form_fields_overrides, question_form_fields_overrides, checkout_confirm_messages, checkout_confirm_page_content, checkout_all_optional, html_page_header, render_seating_plan, checkout_flow_steps, position_info, position_info_top, item_description, global_html_head, global_html_footer, global_html_page_header
|
||||||
|
|
||||||
|
|
||||||
.. automodule:: pretix.presale.signals
|
.. automodule:: pretix.presale.signals
|
||||||
|
|||||||
@@ -19,3 +19,4 @@ Contents:
|
|||||||
permissions
|
permissions
|
||||||
logging
|
logging
|
||||||
locking
|
locking
|
||||||
|
timemachine
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ includes serializers for serializing the following types:
|
|||||||
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
|
* Built-in types: ``int``, ``float``, ``decimal.Decimal``, ``dict``, ``list``, ``bool``
|
||||||
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
|
* ``datetime.date``, ``datetime.datetime``, ``datetime.time``
|
||||||
* ``LazyI18nString``
|
* ``LazyI18nString``
|
||||||
* References to Django ``File`` objects that are already stored in a storage backend
|
* References to Django ``File`` objects that are already stored in a storage backend [#f1]_
|
||||||
* References to model instances
|
* References to model instances
|
||||||
|
|
||||||
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
|
In code, we recommend to always use the ``.get()`` method on the settings object to access a value, but for
|
||||||
@@ -55,6 +55,9 @@ You can simply use it like this:
|
|||||||
"preserve his reservation."),
|
"preserve his reservation."),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
.. _settings-defaults-in-plugins:
|
||||||
|
|
||||||
Defaults in plugins
|
Defaults in plugins
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
@@ -70,3 +73,9 @@ Make sure that you include this code in a module that is imported at app loading
|
|||||||
|
|
||||||
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
.. _django-hierarkey: https://github.com/raphaelm/django-hierarkey
|
||||||
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
.. _documentation: https://django-hierarkey.readthedocs.io/en/latest/
|
||||||
|
|
||||||
|
.. rubric:: Footnotes
|
||||||
|
|
||||||
|
.. [#f1] If you store ``File`` instances in per-event settings, make sure to always register them with ``add_default``
|
||||||
|
as described above in :ref:`settings-defaults-in-plugins`. Otherwise, the file won't get copied properly if the
|
||||||
|
user copies the settings of an existing event to a new one.
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
Time machine mode
|
||||||
|
=================
|
||||||
|
|
||||||
|
In test mode, pretix provides a "time machine" feature which allows event organizers
|
||||||
|
to test their shop as if it were a different date and time. To enable this feature, they can
|
||||||
|
click on the "time machine"-link in the test mode warning box on the event page.
|
||||||
|
|
||||||
|
Internally, this time machine mode is implemented by calling our custom :py:meth:`time_machine_now()`
|
||||||
|
function instead of :py:meth:`django.utils.timezone.now()` in all places where the fake time should be
|
||||||
|
taken into account. If you add code that uses the current date and time for checking whether some
|
||||||
|
product can be bought, you should use :py:meth:`time_machine_now`.
|
||||||
|
|
||||||
|
.. autofunction:: pretix.base.timemachine.time_machine_now
|
||||||
|
|
||||||
|
Background tasks
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The time machine datetime is passed through the request flow via a thread-local variable (ContextVar).
|
||||||
|
Therefore, if you call a background task in the order process, where time_machine_now should be
|
||||||
|
respected, you need to pass it through manually as shown in the example below:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
@app.task()
|
||||||
|
def my_task(self, override_now_dt: datetime=None) -> None:
|
||||||
|
with time_machine_now_assigned(override_now_dt):
|
||||||
|
# ...do something that uses time_machine_now()
|
||||||
|
|
||||||
|
my_task.apply_async(kwargs={'override_now_dt': time_machine_now(default=None)})
|
||||||
|
|
||||||
|
|
||||||
|
.. autofunction:: pretix.base.timemachine.time_machine_now_assigned
|
||||||
@@ -90,6 +90,10 @@ as its first argument and can be used like this::
|
|||||||
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
<a href="{% eventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
||||||
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
<a href="{% abseventurl request.event "presale:event.checkout" step="payment" %}">Pay</a>
|
||||||
|
|
||||||
|
To generate absolute URLs on the main domain, you can use the ``absurl`` template tag::
|
||||||
|
|
||||||
|
{% load eventurl %}
|
||||||
|
<a href="{% absmainurl "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}">Event settings</a>
|
||||||
|
|
||||||
Implementation details
|
Implementation details
|
||||||
----------------------
|
----------------------
|
||||||
|
|||||||
@@ -211,5 +211,15 @@ with the documentation a lot, you might find it useful to use sphinx-autobuild::
|
|||||||
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
|
Then, go to http://localhost:8081 for a version of the documentation that automatically re-builds
|
||||||
whenever you change a source file.
|
whenever you change a source file.
|
||||||
|
|
||||||
|
Working with frontend assets
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
To update the frontend styles of shops with a custom styling, run the following commands inside
|
||||||
|
your virtual environment.::
|
||||||
|
|
||||||
|
python -m pretix collectstatic --noinput
|
||||||
|
python -m pretix updateassets
|
||||||
|
|
||||||
|
|
||||||
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
.. _Django's documentation: https://docs.djangoproject.com/en/1.11/ref/django-admin/#runserver
|
||||||
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
.. _pretixdroid: https://github.com/pretix/pretixdroid
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ pretix/
|
|||||||
Additional code implementing our customized :ref:`URL handling <urlconf>`.
|
Additional code implementing our customized :ref:`URL handling <urlconf>`.
|
||||||
|
|
||||||
static/
|
static/
|
||||||
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core
|
Contains all static files (CSS/SASS, JavaScript, images) of pretix' core.
|
||||||
We use libsass as a preprocessor for CSS. Our own sass code is built in the same
|
We use libsass as a preprocessor for CSS. Our own sass code is built in the same
|
||||||
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
|
step as Bootstrap and FontAwesome, so their mixins etc. are fully available.
|
||||||
|
|
||||||
@@ -41,6 +41,6 @@ pretix/
|
|||||||
|
|
||||||
tests/
|
tests/
|
||||||
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
|
This is the root directory for all test codes. It includes subdirectories ``api``, ``base``,
|
||||||
``control``, ``presale``, ``helpers`, ``multidomain`` and ``plugins`` to mirror the structure
|
``control``, ``presale``, ``helpers``, ``multidomain`` and ``plugins`` to mirror the structure
|
||||||
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
|
of the pretix source code as well as ``testdummy``, which is a pretix plugin used during
|
||||||
testing.
|
testing.
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ allow_voucher_access boolean Enables access
|
|||||||
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
|
lead_scanning_scope_by_device string Enables lead scanning to be handled as one lead per attendee
|
||||||
per scanning device, instead of only per exhibitor.
|
per scanning device, instead of only per exhibitor.
|
||||||
comment string Internal comment, not shown to exhibitor
|
comment string Internal comment, not shown to exhibitor
|
||||||
|
exhibitor_tags list of strings Internal tags to categorize exhibitors, not shown to exhibitor.
|
||||||
|
The tags need to be created through the web interface currently.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
You can also access the scanned leads through the API which contains the following public fields:
|
You can also access the scanned leads through the API which contains the following public fields:
|
||||||
@@ -119,7 +121,8 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -173,7 +176,8 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": []
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to fetch
|
:param organizer: The ``slug`` field of the organizer to fetch
|
||||||
@@ -374,7 +378,10 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": [
|
||||||
|
"Gold Sponsor"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
**Example response**:
|
**Example response**:
|
||||||
@@ -407,7 +414,10 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": [
|
||||||
|
"Gold Sponsor"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
:param organizer: The ``slug`` field of the organizer to create new exhibitor for
|
||||||
@@ -468,7 +478,10 @@ Endpoints
|
|||||||
"allow_lead_scanning": true,
|
"allow_lead_scanning": true,
|
||||||
"allow_lead_access": true,
|
"allow_lead_access": true,
|
||||||
"allow_voucher_access": true,
|
"allow_voucher_access": true,
|
||||||
"comment": ""
|
"comment": "",
|
||||||
|
"exhibitor_tags": [
|
||||||
|
"Gold Sponsor"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
:param organizer: The ``slug`` field of the organizer to modify
|
:param organizer: The ``slug`` field of the organizer to modify
|
||||||
|
|||||||
@@ -339,9 +339,9 @@ Currently, the following attributes are understood by pretix itself:
|
|||||||
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
|
``data-attendee-name``, which will pre-fill the last part of the name, whatever that is.
|
||||||
|
|
||||||
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
|
* ``data-invoice-address-FIELD`` will pre-fill the corresponding field of the invoice address. Possible values for
|
||||||
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city`` and ``country``, as well as fields specified by the
|
``FIELD`` are ``company``, ``street``, ``zipcode``, ``city``, ``country``, ``internal-reference``, ``vat-id``, and
|
||||||
naming scheme such as ``name-title`` or ``name-given-name`` (see above). ``country`` expects a two-character
|
``custom-field``, as well as fields specified by the naming scheme such as ``name-title`` or ``name-given-name``
|
||||||
country code.
|
(see above). ``country`` expects a two-character country code.
|
||||||
|
|
||||||
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
|
* If ``data-fix="true"`` is given, the user will not be able to change the other given values later. This currently
|
||||||
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
only works for the order email address as well as the invoice address. Attendee-level fields and questions can
|
||||||
|
|||||||
+14
-14
@@ -36,15 +36,15 @@ dependencies = [
|
|||||||
"css-inline==0.14.*",
|
"css-inline==0.14.*",
|
||||||
"defusedcsv>=1.1.0",
|
"defusedcsv>=1.1.0",
|
||||||
"dj-static",
|
"dj-static",
|
||||||
"Django==4.2.*",
|
"Django[argon2]==4.2.*",
|
||||||
"django-bootstrap3==24.2",
|
"django-bootstrap3==24.2",
|
||||||
"django-compressor==4.4",
|
"django-compressor==4.5",
|
||||||
"django-countries==7.6.*",
|
"django-countries==7.6.*",
|
||||||
"django-filter==24.2",
|
"django-filter==24.2",
|
||||||
"django-formset-js-improved==0.5.0.3",
|
"django-formset-js-improved==0.5.0.3",
|
||||||
"django-formtools==2.5.1",
|
"django-formtools==2.5.1",
|
||||||
"django-hierarkey==1.1.*",
|
"django-hierarkey==1.2.*",
|
||||||
"django-hijack==3.4.*",
|
"django-hijack==3.5.*",
|
||||||
"django-i18nfield==1.9.*,>=1.9.4",
|
"django-i18nfield==1.9.*,>=1.9.4",
|
||||||
"django-libsass==0.9",
|
"django-libsass==0.9",
|
||||||
"django-localflavor==4.0",
|
"django-localflavor==4.0",
|
||||||
@@ -59,7 +59,7 @@ dependencies = [
|
|||||||
"dnspython==2.6.*",
|
"dnspython==2.6.*",
|
||||||
"drf_ujson2==1.7.*",
|
"drf_ujson2==1.7.*",
|
||||||
"geoip2==4.*",
|
"geoip2==4.*",
|
||||||
"importlib_metadata==7.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||||
"isoweek",
|
"isoweek",
|
||||||
"jsonschema",
|
"jsonschema",
|
||||||
"kombu==5.3.*",
|
"kombu==5.3.*",
|
||||||
@@ -77,12 +77,12 @@ dependencies = [
|
|||||||
"phonenumberslite==8.13.*",
|
"phonenumberslite==8.13.*",
|
||||||
"Pillow==10.3.*",
|
"Pillow==10.3.*",
|
||||||
"pretix-plugin-build",
|
"pretix-plugin-build",
|
||||||
"protobuf==5.26.*",
|
"protobuf==5.27.*",
|
||||||
"psycopg2-binary",
|
"psycopg2-binary",
|
||||||
"pycountry",
|
"pycountry",
|
||||||
"pycparser==2.22",
|
"pycparser==2.22",
|
||||||
"pycryptodome==3.20.*",
|
"pycryptodome==3.20.*",
|
||||||
"pypdf==3.9.*",
|
"pypdf==4.2.*",
|
||||||
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
"python-bidi==0.4.*", # Support for Arabic in reportlab
|
||||||
"python-dateutil==2.9.*",
|
"python-dateutil==2.9.*",
|
||||||
"pytz",
|
"pytz",
|
||||||
@@ -92,7 +92,7 @@ dependencies = [
|
|||||||
"redis==5.0.*",
|
"redis==5.0.*",
|
||||||
"reportlab==4.2.*",
|
"reportlab==4.2.*",
|
||||||
"requests==2.31.*",
|
"requests==2.31.*",
|
||||||
"sentry-sdk==1.45.*",
|
"sentry-sdk==2.5.*",
|
||||||
"sepaxml==2.6.*",
|
"sepaxml==2.6.*",
|
||||||
"slimit",
|
"slimit",
|
||||||
"static3==0.7.*",
|
"static3==0.7.*",
|
||||||
@@ -103,7 +103,7 @@ dependencies = [
|
|||||||
"ua-parser==0.18.*",
|
"ua-parser==0.18.*",
|
||||||
"vat_moss_forked==2020.3.20.0.11.0",
|
"vat_moss_forked==2020.3.20.0.11.0",
|
||||||
"vobject==0.9.*",
|
"vobject==0.9.*",
|
||||||
"webauthn==2.1.*",
|
"webauthn==2.2.*",
|
||||||
"zeep==4.2.*"
|
"zeep==4.2.*"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -113,11 +113,11 @@ dev = [
|
|||||||
"aiohttp==3.9.*",
|
"aiohttp==3.9.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.22.*",
|
"fakeredis==2.23.*",
|
||||||
"flake8==7.0.*",
|
"flake8==7.1.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==5.13.*",
|
"isort==5.13.*",
|
||||||
"pep8-naming==0.13.*",
|
"pep8-naming==0.14.*",
|
||||||
"potypo",
|
"potypo",
|
||||||
"pytest-asyncio",
|
"pytest-asyncio",
|
||||||
"pytest-cache",
|
"pytest-cache",
|
||||||
@@ -126,8 +126,8 @@ dev = [
|
|||||||
"pytest-mock==3.14.*",
|
"pytest-mock==3.14.*",
|
||||||
"pytest-rerunfailures==14.*",
|
"pytest-rerunfailures==14.*",
|
||||||
"pytest-sugar",
|
"pytest-sugar",
|
||||||
"pytest-xdist==3.5.*",
|
"pytest-xdist==3.6.*",
|
||||||
"pytest==8.1.*",
|
"pytest==8.2.*",
|
||||||
"responses",
|
"responses",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2024.4.0"
|
__version__ = "2024.7.0.dev0"
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ ALL_LANGUAGES = [
|
|||||||
('de', _('German')),
|
('de', _('German')),
|
||||||
('de-informal', _('German (informal)')),
|
('de-informal', _('German (informal)')),
|
||||||
('ar', _('Arabic')),
|
('ar', _('Arabic')),
|
||||||
|
('ca', _('Catalan')),
|
||||||
('zh-hans', _('Chinese (simplified)')),
|
('zh-hans', _('Chinese (simplified)')),
|
||||||
('zh-hant', _('Chinese (traditional)')),
|
('zh-hant', _('Chinese (traditional)')),
|
||||||
('cs', _('Czech')),
|
('cs', _('Czech')),
|
||||||
@@ -98,6 +99,7 @@ ALL_LANGUAGES = [
|
|||||||
('pt-br', _('Portuguese (Brazil)')),
|
('pt-br', _('Portuguese (Brazil)')),
|
||||||
('ro', _('Romanian')),
|
('ro', _('Romanian')),
|
||||||
('ru', _('Russian')),
|
('ru', _('Russian')),
|
||||||
|
('sk', _('Slovak')),
|
||||||
('es', _('Spanish')),
|
('es', _('Spanish')),
|
||||||
('tr', _('Turkish')),
|
('tr', _('Turkish')),
|
||||||
('uk', _('Ukrainian')),
|
('uk', _('Ukrainian')),
|
||||||
|
|||||||
@@ -684,8 +684,9 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'locales',
|
'locales',
|
||||||
'locale',
|
'locale',
|
||||||
'region',
|
'region',
|
||||||
'last_order_modification_date',
|
'allow_modifications',
|
||||||
'allow_modifications_after_checkin',
|
'allow_modifications_after_checkin',
|
||||||
|
'last_order_modification_date',
|
||||||
'show_quota_left',
|
'show_quota_left',
|
||||||
'waiting_list_enabled',
|
'waiting_list_enabled',
|
||||||
'waiting_list_auto_disable',
|
'waiting_list_auto_disable',
|
||||||
@@ -735,6 +736,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'payment_term_accept_late',
|
'payment_term_accept_late',
|
||||||
'payment_explanation',
|
'payment_explanation',
|
||||||
'payment_pending_hidden',
|
'payment_pending_hidden',
|
||||||
|
'payment_giftcard__enabled',
|
||||||
'mail_days_order_expire_warning',
|
'mail_days_order_expire_warning',
|
||||||
'ticket_download',
|
'ticket_download',
|
||||||
'ticket_download_date',
|
'ticket_download_date',
|
||||||
|
|||||||
@@ -564,6 +564,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
attendee_name = AttendeeNameField(source='*')
|
attendee_name = AttendeeNameField(source='*')
|
||||||
attendee_name_parts = AttendeeNamePartsField(source='*')
|
attendee_name_parts = AttendeeNamePartsField(source='*')
|
||||||
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
order__status = serializers.SlugRelatedField(read_only=True, slug_field='status', source='order')
|
||||||
|
order__valid_if_pending = serializers.SlugRelatedField(read_only=True, slug_field='valid_if_pending', source='order')
|
||||||
|
order__require_approval = serializers.SlugRelatedField(read_only=True, slug_field='require_approval', source='order')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderPosition
|
model = OrderPosition
|
||||||
@@ -571,7 +573,8 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
|
|||||||
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
'company', 'street', 'zipcode', 'city', 'country', 'state',
|
||||||
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins',
|
||||||
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
'downloads', 'answers', 'tax_rule', 'pseudonymization_id', 'pdf_data', 'seat', 'require_attention',
|
||||||
'order__status', 'valid_from', 'valid_until', 'blocked')
|
'order__status', 'order__valid_if_pending', 'order__require_approval', 'valid_from', 'valid_until',
|
||||||
|
'blocked')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class SettingsSerializer(serializers.Serializer):
|
|||||||
except OSError: # pragma: no cover
|
except OSError: # pragma: no cover
|
||||||
logger.error('Deleting file %s failed.' % fname.name)
|
logger.error('Deleting file %s failed.' % fname.name)
|
||||||
instance.delete(attr)
|
instance.delete(attr)
|
||||||
|
self.changed_data.append(attr)
|
||||||
else:
|
else:
|
||||||
# file is unchanged
|
# file is unchanged
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -57,10 +57,8 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.event import SubEvent
|
from pretix.base.models.event import SubEvent
|
||||||
from pretix.base.services.quotas import QuotaAvailability
|
from pretix.base.services.quotas import QuotaAvailability
|
||||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
from pretix.helpers.i18n import i18ncomp
|
from pretix.helpers.i18n import i18ncomp
|
||||||
from pretix.presale.style import regenerate_css
|
|
||||||
from pretix.presale.views.organizer import filter_qs_by_attr
|
from pretix.presale.views.organizer import filter_qs_by_attr
|
||||||
|
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
@@ -190,7 +188,10 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
serializer = self.get_serializer(page, many=True)
|
serializer = self.get_serializer(page, many=True)
|
||||||
return self.get_paginated_response(serializer.data)
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
|
original_data = self.get_serializer(instance=serializer.instance).data
|
||||||
|
|
||||||
current_live_value = serializer.instance.live
|
current_live_value = serializer.instance.live
|
||||||
updated_live_value = serializer.validated_data.get('live', None)
|
updated_live_value = serializer.validated_data.get('live', None)
|
||||||
current_plugins_value = serializer.instance.get_plugins()
|
current_plugins_value = serializer.instance.get_plugins()
|
||||||
@@ -198,6 +199,11 @@ class EventViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
super().perform_update(serializer)
|
super().perform_update(serializer)
|
||||||
|
|
||||||
|
if serializer.data == original_data:
|
||||||
|
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||||
|
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||||
|
return
|
||||||
|
|
||||||
if updated_live_value is not None and updated_live_value != current_live_value:
|
if updated_live_value is not None and updated_live_value != current_live_value:
|
||||||
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
|
log_action = 'pretix.event.live.activated' if updated_live_value else 'pretix.event.live.deactivated'
|
||||||
serializer.instance.log_action(
|
serializer.instance.log_action(
|
||||||
@@ -622,13 +628,12 @@ class EventSettingsView(views.APIView):
|
|||||||
s.is_valid(raise_exception=True)
|
s.is_valid(raise_exception=True)
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
s.save()
|
s.save()
|
||||||
self.request.event.log_action(
|
if s.changed_data:
|
||||||
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
self.request.event.log_action(
|
||||||
k: v for k, v in s.validated_data.items()
|
'pretix.event.settings', user=self.request.user, auth=self.request.auth, data={
|
||||||
}
|
k: v for k, v in s.validated_data.items()
|
||||||
)
|
}
|
||||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
)
|
||||||
regenerate_css.apply_async(args=(request.event.pk,))
|
|
||||||
s = EventSettingsSerializer(
|
s = EventSettingsSerializer(
|
||||||
instance=request.event.settings, event=request.event, context={
|
instance=request.event.settings, event=request.event, context={
|
||||||
'request': request
|
'request': request
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ from pretix.base.signals import (
|
|||||||
)
|
)
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.control.signals import order_search_filter_q
|
from pretix.control.signals import order_search_filter_q
|
||||||
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -575,8 +576,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
|||||||
return self.retrieve(request, [], **kwargs)
|
return self.retrieve(request, [], **kwargs)
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
|
@transaction.atomic()
|
||||||
def create_invoice(self, request, **kwargs):
|
def create_invoice(self, request, **kwargs):
|
||||||
order = self.get_object()
|
order = self.get_object()
|
||||||
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=order.pk)
|
||||||
has_inv = order.invoices.exists() and not (
|
has_inv = order.invoices.exists() and not (
|
||||||
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||||
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||||
@@ -1905,6 +1908,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
@action(detail=True, methods=['POST'])
|
@action(detail=True, methods=['POST'])
|
||||||
|
@transaction.atomic()
|
||||||
def reissue(self, request, **kwargs):
|
def reissue(self, request, **kwargs):
|
||||||
inv = self.get_object()
|
inv = self.get_object()
|
||||||
if inv.canceled:
|
if inv.canceled:
|
||||||
@@ -1912,9 +1916,10 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
elif inv.shredded:
|
elif inv.shredded:
|
||||||
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
raise PermissionDenied('The invoice file is no longer stored on the server.')
|
||||||
else:
|
else:
|
||||||
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
|
||||||
c = generate_cancellation(inv)
|
c = generate_cancellation(inv)
|
||||||
if inv.order.status != Order.STATUS_CANCELED:
|
if inv.order.status != Order.STATUS_CANCELED:
|
||||||
inv = generate_invoice(inv.order)
|
inv = generate_invoice(order)
|
||||||
else:
|
else:
|
||||||
inv = c
|
inv = c
|
||||||
inv.order.log_action(
|
inv.order.log_action(
|
||||||
|
|||||||
@@ -51,10 +51,8 @@ from pretix.base.models import (
|
|||||||
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
MembershipType, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.dicts import merge_dicts
|
from pretix.helpers.dicts import merge_dicts
|
||||||
from pretix.presale.style import regenerate_organizer_css
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
@@ -504,8 +502,6 @@ class OrganizerSettingsView(views.APIView):
|
|||||||
k: v for k, v in s.validated_data.items()
|
k: v for k, v in s.validated_data.items()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if any(p in s.changed_data for p in SETTINGS_AFFECTING_CSS):
|
|
||||||
regenerate_organizer_css.apply_async(args=(request.organizer.pk,))
|
|
||||||
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
|
||||||
'request': request
|
'request': request
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -39,10 +39,12 @@ from zipfile import ZipFile
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
|
|
||||||
from pretix.base.models import QuestionAnswer
|
from pretix.base.models import QuestionAnswer
|
||||||
|
|
||||||
|
from ...control.forms.widgets import Select2
|
||||||
from ..exporter import BaseExporter
|
from ..exporter import BaseExporter
|
||||||
from ..signals import register_data_exporters
|
from ..signals import register_data_exporters
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@ class AnswerFilesExporter(BaseExporter):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def export_form_fields(self):
|
def export_form_fields(self):
|
||||||
return OrderedDict(
|
d = OrderedDict(
|
||||||
[
|
[
|
||||||
('questions',
|
('questions',
|
||||||
forms.ModelMultipleChoiceField(
|
forms.ModelMultipleChoiceField(
|
||||||
@@ -69,11 +71,32 @@ class AnswerFilesExporter(BaseExporter):
|
|||||||
)),
|
)),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
if self.event.has_subevents:
|
||||||
|
d['subevent'] = forms.ModelChoiceField(
|
||||||
|
label=pgettext_lazy('subevent', 'Date'),
|
||||||
|
queryset=self.event.subevents.all(),
|
||||||
|
required=False,
|
||||||
|
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||||
|
)
|
||||||
|
d['subevent'].widget = Select2(
|
||||||
|
attrs={
|
||||||
|
'data-model-select2': 'event',
|
||||||
|
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||||
|
'event': self.event.slug,
|
||||||
|
'organizer': self.event.organizer.slug,
|
||||||
|
}),
|
||||||
|
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
d['subevent'].widget.choices = d['subevent'].choices
|
||||||
|
return d
|
||||||
|
|
||||||
def render(self, form_data: dict):
|
def render(self, form_data: dict):
|
||||||
qs = QuestionAnswer.objects.filter(
|
qs = QuestionAnswer.objects.filter(
|
||||||
orderposition__order__event=self.event,
|
orderposition__order__event=self.event,
|
||||||
).select_related('orderposition', 'orderposition__order', 'question')
|
).select_related('orderposition', 'orderposition__order', 'question')
|
||||||
|
if form_data.get('subevent'):
|
||||||
|
qs = qs.filter(orderposition__subevent=form_data.get('subevent'))
|
||||||
if form_data.get('questions'):
|
if form_data.get('questions'):
|
||||||
qs = qs.filter(question__in=form_data['questions'])
|
qs = qs.filter(question__in=form_data['questions'])
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
|||||||
@@ -32,11 +32,12 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict, defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
|
||||||
Q, Subquery, Sum, When,
|
Q, Subquery, Sum, When,
|
||||||
@@ -54,7 +55,7 @@ from openpyxl.comments import Comment
|
|||||||
from openpyxl.styles import Font, PatternFill
|
from openpyxl.styles import Font, PatternFill
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
Checkin, GiftCard, GiftCardTransaction, Invoice, InvoiceAddress, Order,
|
||||||
OrderPosition, Question,
|
OrderPosition, Question,
|
||||||
)
|
)
|
||||||
from pretix.base.models.orders import (
|
from pretix.base.models.orders import (
|
||||||
@@ -541,6 +542,22 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
).order_by()
|
).order_by()
|
||||||
qs = base_qs.annotate(
|
qs = base_qs.annotate(
|
||||||
payment_providers=Subquery(p_providers, output_field=CharField()),
|
payment_providers=Subquery(p_providers, output_field=CharField()),
|
||||||
|
checked_in_lists=Subquery(
|
||||||
|
Checkin.objects.filter(
|
||||||
|
successful=True,
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
position=OuterRef("pk"),
|
||||||
|
).order_by().values("position").annotate(
|
||||||
|
c=GroupConcat(
|
||||||
|
"list__name",
|
||||||
|
# These appear not to work properly on SQLite. Well, we don't support SQLite outside testing
|
||||||
|
# anyways.
|
||||||
|
ordered='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||||
|
distinct='sqlite' not in settings.DATABASES['default']['ENGINE'],
|
||||||
|
delimiter=", "
|
||||||
|
)
|
||||||
|
).values("c")
|
||||||
|
),
|
||||||
).select_related(
|
).select_related(
|
||||||
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
'order', 'order__invoice_address', 'order__customer', 'item', 'variation',
|
||||||
'voucher', 'tax_rule'
|
'voucher', 'tax_rule'
|
||||||
@@ -605,10 +622,9 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
]
|
]
|
||||||
|
|
||||||
questions = list(Question.objects.filter(event__in=self.events))
|
questions = list(Question.objects.filter(event__in=self.events))
|
||||||
options = {}
|
options = defaultdict(list)
|
||||||
for q in questions:
|
for q in questions:
|
||||||
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
if q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||||
options[q.pk] = []
|
|
||||||
if form_data['group_multiple_choice']:
|
if form_data['group_multiple_choice']:
|
||||||
for o in q.options.all():
|
for o in q.options.all():
|
||||||
options[q.pk].append(o)
|
options[q.pk].append(o)
|
||||||
@@ -618,6 +634,9 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
headers.append(str(q.question) + ' – ' + str(o.answer))
|
headers.append(str(q.question) + ' – ' + str(o.answer))
|
||||||
options[q.pk].append(o)
|
options[q.pk].append(o)
|
||||||
else:
|
else:
|
||||||
|
if q.type == Question.TYPE_CHOICE:
|
||||||
|
for o in q.options.all():
|
||||||
|
options[q.pk].append(o)
|
||||||
headers.append(str(q.question))
|
headers.append(str(q.question))
|
||||||
headers += [
|
headers += [
|
||||||
_('Company'),
|
_('Company'),
|
||||||
@@ -636,6 +655,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('Sales channel'), _('Order locale'),
|
_('Sales channel'), _('Order locale'),
|
||||||
_('E-mail address verified'),
|
_('E-mail address verified'),
|
||||||
_('External customer ID'),
|
_('External customer ID'),
|
||||||
|
_('Check-in lists'),
|
||||||
_('Payment providers'),
|
_('Payment providers'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -727,7 +747,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
for a in op.answers.all():
|
for a in op.answers.all():
|
||||||
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
# We do not want to localize Date, Time and Datetime question answers, as those can lead
|
||||||
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
# to difficulties parsing the data (for example 2019-02-01 may become Février, 2019 01 in French).
|
||||||
if a.question.type == Question.TYPE_CHOICE_MULTIPLE:
|
if a.question.type in (Question.TYPE_CHOICE_MULTIPLE, Question.TYPE_CHOICE):
|
||||||
acache[a.question_id] = set(o.pk for o in a.options.all())
|
acache[a.question_id] = set(o.pk for o in a.options.all())
|
||||||
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
elif a.question.type in Question.UNLOCALIZED_TYPES:
|
||||||
acache[a.question_id] = a.answer
|
acache[a.question_id] = a.answer
|
||||||
@@ -740,6 +760,10 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
else:
|
else:
|
||||||
for o in options[q.pk]:
|
for o in options[q.pk]:
|
||||||
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
row.append(_('Yes') if o.pk in acache.get(q.pk, set()) else _('No'))
|
||||||
|
elif q.type == Question.TYPE_CHOICE:
|
||||||
|
# Join is only necessary if the question type was modified but also keeps the code simpler here
|
||||||
|
# as we'd otherwise need some [0] and existance checks
|
||||||
|
row.append(", ".join(str(o.answer) for o in options[q.pk] if o.pk in acache.get(q.pk, set())))
|
||||||
else:
|
else:
|
||||||
row.append(acache.get(q.pk, ''))
|
row.append(acache.get(q.pk, ''))
|
||||||
|
|
||||||
@@ -770,6 +794,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
_('Yes') if order.email_known_to_work else _('No'),
|
_('Yes') if order.email_known_to_work else _('No'),
|
||||||
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
str(order.customer.external_identifier) if order.customer and order.customer.external_identifier else '',
|
||||||
]
|
]
|
||||||
|
row.append(op.checked_in_lists or "")
|
||||||
row.append(', '.join([
|
row.append(', '.join([
|
||||||
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
str(self.providers.get(p, p)) for p in sorted(set((op.payment_providers or '').split(',')))
|
||||||
if p and p != 'free'
|
if p and p != 'free'
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
|||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.timezone import get_current_timezone, now
|
from django.utils.timezone import get_current_timezone
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries import countries
|
from django_countries import countries
|
||||||
from django_countries.fields import Country, CountryField
|
from django_countries.fields import Country, CountryField
|
||||||
@@ -86,6 +86,7 @@ from pretix.base.settings import (
|
|||||||
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
|
||||||
)
|
)
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.control.forms import (
|
from pretix.control.forms import (
|
||||||
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
|
||||||
)
|
)
|
||||||
@@ -606,13 +607,13 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
|
|
||||||
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||||
if item.validity_dynamic_start_choice_day_limit:
|
if item.validity_dynamic_start_choice_day_limit:
|
||||||
max_date = now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
max_date = time_machine_now().astimezone(event.timezone) + timedelta(days=item.validity_dynamic_start_choice_day_limit)
|
||||||
else:
|
else:
|
||||||
max_date = None
|
max_date = None
|
||||||
min_date = now()
|
min_date = time_machine_now()
|
||||||
initial = None
|
initial = None
|
||||||
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
|
if (item.require_membership or (pos.variation and pos.variation.require_membership)) and pos.used_membership:
|
||||||
if pos.used_membership.date_start >= now():
|
if pos.used_membership.date_start >= time_machine_now():
|
||||||
initial = min_date = pos.used_membership.date_start
|
initial = min_date = pos.used_membership.date_start
|
||||||
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
|
max_date = min(max_date, pos.used_membership.date_end) if max_date else pos.used_membership.date_end
|
||||||
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
|
if item.validity_dynamic_duration_months or item.validity_dynamic_duration_days:
|
||||||
@@ -1034,7 +1035,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
self.all_optional = kwargs.pop('all_optional', False)
|
self.all_optional = kwargs.pop('all_optional', False)
|
||||||
|
|
||||||
kwargs.setdefault('initial', {})
|
kwargs.setdefault('initial', {})
|
||||||
if not kwargs.get('instance') or not kwargs['instance'].country:
|
if (not kwargs.get('instance') or not kwargs['instance'].country) and not kwargs["initial"].get("country"):
|
||||||
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
kwargs['initial']['country'] = guess_country_from_request(self.request, self.event)
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@@ -1170,7 +1171,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
|
|||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
messages.warning(self.request, e.message)
|
messages.warning(self.request, e.message)
|
||||||
else:
|
else:
|
||||||
raise ValidationError(e.message)
|
raise ValidationError({"vat_id": e.message})
|
||||||
except VATIDTemporaryError as e:
|
except VATIDTemporaryError as e:
|
||||||
self.instance.vat_id_validated = False
|
self.instance.vat_id_validated = False
|
||||||
if self.request and self.vat_warning:
|
if self.request and self.vat_warning:
|
||||||
|
|||||||
@@ -32,13 +32,13 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import BaseValidator
|
from django.core.validators import BaseValidator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
|
|
||||||
|
from pretix.helpers.format import format_map
|
||||||
|
|
||||||
|
|
||||||
class PlaceholderValidator(BaseValidator):
|
class PlaceholderValidator(BaseValidator):
|
||||||
"""
|
"""
|
||||||
@@ -47,6 +47,12 @@ class PlaceholderValidator(BaseValidator):
|
|||||||
which are not presented in taken list.
|
which are not presented in taken list.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
error_message = _(
|
||||||
|
'There is an error with your placeholder syntax. Please check that the opening "{" and closing "}" curly '
|
||||||
|
'brackets on your placeholders match up. '
|
||||||
|
'Please note: to use literal "{" or "}", you need to double them as "{{" and "}}".'
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, limit_value):
|
def __init__(self, limit_value):
|
||||||
super().__init__(limit_value)
|
super().__init__(limit_value)
|
||||||
self.limit_value = limit_value
|
self.limit_value = limit_value
|
||||||
@@ -57,22 +63,15 @@ class PlaceholderValidator(BaseValidator):
|
|||||||
self.__call__(v)
|
self.__call__(v)
|
||||||
return
|
return
|
||||||
|
|
||||||
if value.count('{') != value.count('}'):
|
try:
|
||||||
|
format_map(value, {key.strip('{}'): "" for key in self.limit_value}, raise_on_missing=True)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(self.error_message, code='invalid_placeholder_syntax')
|
||||||
|
except KeyError as e:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Invalid placeholder syntax: You used a different number of "{" than of "}".'),
|
_('Invalid placeholder: {%(value)s}'),
|
||||||
code='invalid_placeholder_syntax',
|
|
||||||
)
|
|
||||||
|
|
||||||
data_placeholders = list(re.findall(r'({[^}]*})', value, re.X))
|
|
||||||
invalid_placeholders = []
|
|
||||||
for placeholder in data_placeholders:
|
|
||||||
if placeholder not in self.limit_value:
|
|
||||||
invalid_placeholders.append(placeholder)
|
|
||||||
if invalid_placeholders:
|
|
||||||
raise ValidationError(
|
|
||||||
_('Invalid placeholder(s): %(value)s'),
|
|
||||||
code='invalid_placeholders',
|
code='invalid_placeholders',
|
||||||
params={'value': ", ".join(invalid_placeholders,)})
|
params={'value': e.args[0]})
|
||||||
|
|
||||||
def clean(self, x):
|
def clean(self, x):
|
||||||
return x
|
return x
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 4.2.11 on 2024-05-16 11:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import pretix.base.models.orders
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0263_auto_20240409_0732"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="order",
|
||||||
|
name="internal_secret",
|
||||||
|
field=models.CharField(
|
||||||
|
default=None,
|
||||||
|
max_length=32,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import pycountry
|
import pycountry
|
||||||
@@ -26,6 +27,7 @@ from django.conf import settings
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import EmailValidator
|
from django.core.validators import EmailValidator
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.utils import formats
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.translation import (
|
from django.utils.translation import (
|
||||||
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
gettext as _, gettext_lazy, pgettext, pgettext_lazy,
|
||||||
@@ -509,6 +511,30 @@ class ValidUntil(DatetimeColumnMixin, ImportColumn):
|
|||||||
position.valid_until = value
|
position.valid_until = value
|
||||||
|
|
||||||
|
|
||||||
|
class Expires(DatetimeColumnMixin, ImportColumn):
|
||||||
|
identifier = 'expires'
|
||||||
|
verbose_name = gettext_lazy('Expiry date')
|
||||||
|
|
||||||
|
def clean(self, value, previous_values):
|
||||||
|
if not value:
|
||||||
|
return
|
||||||
|
|
||||||
|
input_formats = formats.get_format('DATE_INPUT_FORMATS', use_l10n=True)
|
||||||
|
for format in input_formats:
|
||||||
|
try:
|
||||||
|
d = datetime.datetime.strptime(value, format)
|
||||||
|
d = d.replace(tzinfo=self.timezone, hour=23, minute=59, second=59)
|
||||||
|
return d
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return super().clean(value, previous_values) # parse date
|
||||||
|
|
||||||
|
def assign(self, value, order, position, invoice_address, **kwargs):
|
||||||
|
if value:
|
||||||
|
order.expires = value
|
||||||
|
|
||||||
|
|
||||||
class Saleschannel(ImportColumn):
|
class Saleschannel(ImportColumn):
|
||||||
identifier = 'sales_channel'
|
identifier = 'sales_channel'
|
||||||
verbose_name = gettext_lazy('Sales channel')
|
verbose_name = gettext_lazy('Sales channel')
|
||||||
@@ -702,12 +728,13 @@ def get_order_import_columns(event):
|
|||||||
AttendeeState(event),
|
AttendeeState(event),
|
||||||
Price(event),
|
Price(event),
|
||||||
Secret(event),
|
Secret(event),
|
||||||
Locale(event),
|
|
||||||
Saleschannel(event),
|
|
||||||
SeatColumn(event),
|
SeatColumn(event),
|
||||||
Comment(event),
|
|
||||||
ValidFrom(event),
|
ValidFrom(event),
|
||||||
ValidUntil(event),
|
ValidUntil(event),
|
||||||
|
Locale(event),
|
||||||
|
Saleschannel(event),
|
||||||
|
Expires(event),
|
||||||
|
Comment(event),
|
||||||
]
|
]
|
||||||
for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE):
|
for q in event.questions.prefetch_related('options').exclude(type=Question.TYPE_FILE):
|
||||||
default.append(QuestionColumn(event, q))
|
default.append(QuestionColumn(event, q))
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ class CodeColumn(ImportColumn):
|
|||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if not value:
|
|
||||||
raise ValidationError(_('A voucher cannot be created without a code.'))
|
|
||||||
if value:
|
if value:
|
||||||
MinLengthValidator(5)(value)
|
MinLengthValidator(5)(value)
|
||||||
if value and (value in self._cached or Voucher.objects.filter(event=self.event, code=value).exists()):
|
if value and (value in self._cached or Voucher.objects.filter(event=self.event, code=value).exists()):
|
||||||
@@ -77,7 +75,7 @@ class MaxUsagesColumn(IntegerColumnMixin, ImportColumn):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value is None:
|
if value is None and previous_values.get("code"):
|
||||||
raise ValidationError(_('The maximum number of usages must be set.'))
|
raise ValidationError(_('The maximum number of usages must be set.'))
|
||||||
return super().clean(value, previous_values)
|
return super().clean(value, previous_values)
|
||||||
|
|
||||||
|
|||||||
@@ -418,18 +418,22 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
|||||||
else:
|
else:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if this user is part of any team that grants access of type ``perm_name``
|
Checks if this user is part of any team that grants access of type ``perm_name``
|
||||||
to the event ``event``.
|
to the event ``event``.
|
||||||
|
|
||||||
|
Either ``request`` or ``session_key`` are required to detect staff sessions properly.
|
||||||
|
|
||||||
:param organizer: The organizer of the event
|
:param organizer: The organizer of the event
|
||||||
:param event: The event to check
|
:param event: The event to check
|
||||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||||
:param request: The current request (optional). Required to detect staff sessions properly.
|
:param request: The current request (optional)
|
||||||
|
:param session_key: The current session key (optional)
|
||||||
:return: bool
|
:return: bool
|
||||||
"""
|
"""
|
||||||
if request and self.has_active_staff_session(request.session.session_key):
|
assert not (session_key and request)
|
||||||
|
if (session_key or request) and self.has_active_staff_session(session_key or request.session.session_key):
|
||||||
return True
|
return True
|
||||||
teams = self._get_teams_for_event(organizer, event)
|
teams = self._get_teams_for_event(organizer, event)
|
||||||
if teams:
|
if teams:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
from math import ceil
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@@ -272,7 +273,7 @@ class Discount(LoggedModel):
|
|||||||
|
|
||||||
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
|
||||||
# want to match multiples of 3
|
# want to match multiples of 3
|
||||||
n_groups = min(len(condition_idx_group) // self.condition_min_count, len(benefit_idx_group))
|
n_groups = min(len(condition_idx_group) // self.condition_min_count, ceil(len(benefit_idx_group) / self.benefit_only_apply_to_cheapest_n_matches))
|
||||||
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
|
consume_idx = condition_idx_group[:n_groups * self.condition_min_count]
|
||||||
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
|
benefit_idx = benefit_idx_group[:n_groups * self.benefit_only_apply_to_cheapest_n_matches]
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ from zoneinfo import ZoneInfo
|
|||||||
import pytz_deprecation_shim
|
import pytz_deprecation_shim
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.files import File
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.core.mail import get_connection
|
from django.core.mail import get_connection
|
||||||
from django.core.validators import (
|
from django.core.validators import (
|
||||||
@@ -67,6 +68,7 @@ from i18nfield.fields import I18nCharField, I18nTextField
|
|||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.fields import MultiStringField
|
from pretix.base.models.fields import MultiStringField
|
||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.base.validators import EventSlugBanlistValidator
|
from pretix.base.validators import EventSlugBanlistValidator
|
||||||
from pretix.helpers.database import GroupConcat
|
from pretix.helpers.database import GroupConcat
|
||||||
from pretix.helpers.daterange import daterange
|
from pretix.helpers.daterange import daterange
|
||||||
@@ -234,7 +236,7 @@ class EventMixin:
|
|||||||
if not self.settings.waiting_list_enabled:
|
if not self.settings.waiting_list_enabled:
|
||||||
return False
|
return False
|
||||||
if self.settings.waiting_list_auto_disable:
|
if self.settings.waiting_list_auto_disable:
|
||||||
return self.settings.waiting_list_auto_disable.datetime(self) > now()
|
return self.settings.waiting_list_auto_disable.datetime(self) > time_machine_now()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -243,11 +245,11 @@ class EventMixin:
|
|||||||
Is true, when ``presale_end`` is set and in the past.
|
Is true, when ``presale_end`` is set and in the past.
|
||||||
"""
|
"""
|
||||||
if self.effective_presale_end:
|
if self.effective_presale_end:
|
||||||
return now() > self.effective_presale_end
|
return time_machine_now() > self.effective_presale_end
|
||||||
elif self.date_to:
|
elif self.date_to:
|
||||||
return now() > self.date_to
|
return time_machine_now() > self.date_to
|
||||||
else:
|
else:
|
||||||
return now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
return time_machine_now().astimezone(self.timezone).date() > self.date_from.astimezone(self.timezone).date()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def effective_presale_start(self):
|
def effective_presale_start(self):
|
||||||
@@ -267,7 +269,7 @@ class EventMixin:
|
|||||||
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
|
Is true, when ``presale_end`` is not set or in the future and ``presale_start`` is not
|
||||||
set or in the past.
|
set or in the past.
|
||||||
"""
|
"""
|
||||||
if self.effective_presale_start and now() < self.effective_presale_start:
|
if self.effective_presale_start and time_machine_now() < self.effective_presale_start:
|
||||||
return False
|
return False
|
||||||
return not self.presale_has_ended
|
return not self.presale_has_ended
|
||||||
|
|
||||||
@@ -315,11 +317,11 @@ class EventMixin:
|
|||||||
q_variation = (
|
q_variation = (
|
||||||
Q(active=True)
|
Q(active=True)
|
||||||
& Q(sales_channels__contains=channel)
|
& Q(sales_channels__contains=channel)
|
||||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
|
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()))
|
||||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
|
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()))
|
||||||
& Q(item__active=True)
|
& Q(item__active=True)
|
||||||
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now()))
|
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=time_machine_now()))
|
||||||
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
|
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=time_machine_now()))
|
||||||
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
|
||||||
& Q(item__sales_channels__contains=channel)
|
& Q(item__sales_channels__contains=channel)
|
||||||
& Q(item__require_bundling=False)
|
& Q(item__require_bundling=False)
|
||||||
@@ -694,7 +696,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
@property
|
@property
|
||||||
def presale_has_ended(self):
|
def presale_has_ended(self):
|
||||||
if self.has_subevents:
|
if self.has_subevents:
|
||||||
return self.presale_end and now() > self.presale_end
|
return self.presale_end and time_machine_now() > self.presale_end
|
||||||
else:
|
else:
|
||||||
return super().presale_has_ended
|
return super().presale_has_ended
|
||||||
|
|
||||||
@@ -787,8 +789,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
), tz)
|
), tz)
|
||||||
|
|
||||||
def copy_data_from(self, other, skip_meta_data=False):
|
def copy_data_from(self, other, skip_meta_data=False):
|
||||||
from pretix.presale.style import regenerate_css
|
|
||||||
|
|
||||||
from ..signals import event_copy_data
|
from ..signals import event_copy_data
|
||||||
from . import (
|
from . import (
|
||||||
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
|
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
|
||||||
@@ -1009,10 +1009,10 @@ class Event(EventMixin, LoggedModel):
|
|||||||
s.product = item_map[s.product_id]
|
s.product = item_map[s.product_id]
|
||||||
s.save(force_insert=True)
|
s.save(force_insert=True)
|
||||||
|
|
||||||
has_custom_style = other.settings.presale_css_file or other.settings.presale_widget_css_file
|
|
||||||
skip_settings = (
|
skip_settings = (
|
||||||
'ticket_secrets_pretix_sig1_pubkey',
|
'ticket_secrets_pretix_sig1_pubkey',
|
||||||
'ticket_secrets_pretix_sig1_privkey',
|
'ticket_secrets_pretix_sig1_privkey',
|
||||||
|
# no longer used, but we still don't need to copy them
|
||||||
'presale_css_file',
|
'presale_css_file',
|
||||||
'presale_css_checksum',
|
'presale_css_checksum',
|
||||||
'presale_widget_css_file',
|
'presale_widget_css_file',
|
||||||
@@ -1025,7 +1025,7 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
s.object = self
|
s.object = self
|
||||||
s.pk = None
|
s.pk = None
|
||||||
if s.value.startswith('file://'):
|
if s.value.startswith('file://') and settings_hierarkey.get_declared_type(s.key) == File:
|
||||||
fi = default_storage.open(s.value[len('file://'):], 'rb')
|
fi = default_storage.open(s.value[len('file://'):], 'rb')
|
||||||
nonce = get_random_string(length=8)
|
nonce = get_random_string(length=8)
|
||||||
fname_base = clean_filename(os.path.basename(s.value))
|
fname_base = clean_filename(os.path.basename(s.value))
|
||||||
@@ -1055,9 +1055,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
question_map=question_map, checkin_list_map=checkin_list_map, quota_map=quota_map,
|
question_map=question_map, checkin_list_map=checkin_list_map, quota_map=quota_map,
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_custom_style:
|
|
||||||
regenerate_css.apply_async(args=(self.pk,))
|
|
||||||
|
|
||||||
def get_payment_providers(self, cached=False) -> dict:
|
def get_payment_providers(self, cached=False) -> dict:
|
||||||
"""
|
"""
|
||||||
Returns a dictionary of initialized payment providers mapped by their identifiers.
|
Returns a dictionary of initialized payment providers mapped by their identifiers.
|
||||||
@@ -1187,8 +1184,8 @@ class Event(EventMixin, LoggedModel):
|
|||||||
)
|
)
|
||||||
).filter(
|
).filter(
|
||||||
Q(active=True) & Q(is_public=True) & (
|
Q(active=True) & Q(is_public=True) & (
|
||||||
Q(Q(date_to__isnull=True) & Q(date_from__gte=now() - timedelta(hours=24)))
|
Q(Q(date_to__isnull=True) & Q(date_from__gte=time_machine_now() - timedelta(hours=24)))
|
||||||
| Q(date_to__gte=now() - timedelta(hours=24))
|
| Q(date_to__gte=time_machine_now() - timedelta(hours=24))
|
||||||
)
|
)
|
||||||
) # order_by doesn't make sense with I18nField
|
) # order_by doesn't make sense with I18nField
|
||||||
if ordering in ("date_ascending", "date_descending"):
|
if ordering in ("date_ascending", "date_descending"):
|
||||||
@@ -1335,18 +1332,12 @@ class Event(EventMixin, LoggedModel):
|
|||||||
|
|
||||||
def enable_plugin(self, module, allow_restricted=frozenset()):
|
def enable_plugin(self, module, allow_restricted=frozenset()):
|
||||||
plugins_active = self.get_plugins()
|
plugins_active = self.get_plugins()
|
||||||
from pretix.presale.style import regenerate_css
|
|
||||||
|
|
||||||
if module not in plugins_active:
|
if module not in plugins_active:
|
||||||
plugins_active.append(module)
|
plugins_active.append(module)
|
||||||
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
||||||
|
|
||||||
regenerate_css.apply_async(args=(self.pk,))
|
|
||||||
|
|
||||||
def disable_plugin(self, module):
|
def disable_plugin(self, module):
|
||||||
plugins_active = self.get_plugins()
|
plugins_active = self.get_plugins()
|
||||||
from pretix.presale.style import regenerate_css
|
|
||||||
|
|
||||||
if module in plugins_active:
|
if module in plugins_active:
|
||||||
plugins_active.remove(module)
|
plugins_active.remove(module)
|
||||||
self.set_active_plugins(plugins_active)
|
self.set_active_plugins(plugins_active)
|
||||||
@@ -1355,8 +1346,6 @@ class Event(EventMixin, LoggedModel):
|
|||||||
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
|
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
|
||||||
getattr(plugins_available[module].app, 'uninstalled')(self)
|
getattr(plugins_available[module].app, 'uninstalled')(self)
|
||||||
|
|
||||||
regenerate_css.apply_async(args=(self.pk,))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_has_subevents(event, has_subevents):
|
def clean_has_subevents(event, has_subevents):
|
||||||
if event is not None and event.has_subevents is not None:
|
if event is not None and event.has_subevents is not None:
|
||||||
@@ -1466,8 +1455,6 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
|
seating_plan = models.ForeignKey('SeatingPlan', on_delete=models.PROTECT, null=True, blank=True,
|
||||||
related_name='subevents', verbose_name=_('Seating plan'))
|
related_name='subevents', verbose_name=_('Seating plan'))
|
||||||
|
|
||||||
items = models.ManyToManyField('Item', through='SubEventItem')
|
|
||||||
variations = models.ManyToManyField('ItemVariation', through='SubEventItemVariation')
|
|
||||||
comment = models.TextField(
|
comment = models.TextField(
|
||||||
verbose_name=_("Internal comment"),
|
verbose_name=_("Internal comment"),
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
@@ -1508,7 +1495,7 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
disabled_items=Coalesce(
|
disabled_items=Coalesce(
|
||||||
Subquery(
|
Subquery(
|
||||||
SubEventItem.objects.filter(
|
SubEventItem.objects.filter(
|
||||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
|
||||||
subevent=OuterRef('pk'),
|
subevent=OuterRef('pk'),
|
||||||
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
|
).order_by().values('subevent').annotate(items=GroupConcat('item_id', delimiter=',')).values('items'),
|
||||||
output_field=models.TextField(),
|
output_field=models.TextField(),
|
||||||
@@ -1519,7 +1506,7 @@ class SubEvent(EventMixin, LoggedModel):
|
|||||||
disabled_vars=Coalesce(
|
disabled_vars=Coalesce(
|
||||||
Subquery(
|
Subquery(
|
||||||
SubEventItemVariation.objects.filter(
|
SubEventItemVariation.objects.filter(
|
||||||
Q(disabled=True) | Q(available_from__gt=now()) | Q(available_until__lt=now()),
|
Q(disabled=True) | Q(available_from__gt=time_machine_now()) | Q(available_until__lt=time_machine_now()),
|
||||||
subevent=OuterRef('pk'),
|
subevent=OuterRef('pk'),
|
||||||
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
|
).order_by().values('subevent').annotate(items=GroupConcat('variation_id', delimiter=',')).values('items'),
|
||||||
output_field=models.TextField(),
|
output_field=models.TextField(),
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ from django.db.models import Q
|
|||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import is_naive, make_aware, now
|
from django.utils.timezone import is_naive, make_aware
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import ScopedManager
|
from django_scopes import ScopedManager
|
||||||
@@ -65,6 +65,7 @@ from pretix.base.models import fields
|
|||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.fields import MultiStringField
|
from pretix.base.models.fields import MultiStringField
|
||||||
from pretix.base.models.tax import TaxedPrice
|
from pretix.base.models.tax import TaxedPrice
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
|
|
||||||
from ...helpers.images import ImageSizeValidator
|
from ...helpers.images import ImageSizeValidator
|
||||||
from ..media import MEDIA_TYPES
|
from ..media import MEDIA_TYPES
|
||||||
@@ -121,6 +122,16 @@ class ItemCategory(LoggedModel):
|
|||||||
return _('{category} (Add-On products)').format(category=str(name))
|
return _('{category} (Add-On products)').format(category=str(name))
|
||||||
return str(name)
|
return str(name)
|
||||||
|
|
||||||
|
def get_category_type_display(self):
|
||||||
|
if self.is_addon:
|
||||||
|
return _('Add-On products')
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category_type(self):
|
||||||
|
return 'addon' if self.is_addon else 'normal'
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
super().delete(*args, **kwargs)
|
super().delete(*args, **kwargs)
|
||||||
if self.event:
|
if self.event:
|
||||||
@@ -192,7 +203,7 @@ class SubEventItem(models.Model):
|
|||||||
self.subevent.event.cache.clear()
|
self.subevent.event.cache.clear()
|
||||||
|
|
||||||
def is_available(self, now_dt: datetime=None) -> bool:
|
def is_available(self, now_dt: datetime=None) -> bool:
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
if self.disabled:
|
if self.disabled:
|
||||||
return False
|
return False
|
||||||
if self.available_from and self.available_from > now_dt:
|
if self.available_from and self.available_from > now_dt:
|
||||||
@@ -248,7 +259,7 @@ class SubEventItemVariation(models.Model):
|
|||||||
self.subevent.event.cache.clear()
|
self.subevent.event.cache.clear()
|
||||||
|
|
||||||
def is_available(self, now_dt: datetime=None) -> bool:
|
def is_available(self, now_dt: datetime=None) -> bool:
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
if self.disabled:
|
if self.disabled:
|
||||||
return False
|
return False
|
||||||
if self.available_from and self.available_from > now_dt:
|
if self.available_from and self.available_from > now_dt:
|
||||||
@@ -263,8 +274,8 @@ def filter_available(qs, channel='web', voucher=None, allow_addons=False):
|
|||||||
# IMPORTANT: If this is updated, also update the ItemVariation query
|
# IMPORTANT: If this is updated, also update the ItemVariation query
|
||||||
# in models/event.py: EventMixin.annotated()
|
# in models/event.py: EventMixin.annotated()
|
||||||
Q(active=True)
|
Q(active=True)
|
||||||
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()) | Q(available_from_mode='info'))
|
& Q(Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()) | Q(available_from_mode='info'))
|
||||||
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()) | Q(available_until_mode='info'))
|
& Q(Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()) | Q(available_until_mode='info'))
|
||||||
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
& Q(sales_channels__contains=channel) & Q(require_bundling=False)
|
||||||
)
|
)
|
||||||
if not allow_addons:
|
if not allow_addons:
|
||||||
@@ -443,7 +454,8 @@ class Item(LoggedModel):
|
|||||||
free_price_suggestion = models.DecimalField(
|
free_price_suggestion = models.DecimalField(
|
||||||
verbose_name=_("Suggested price"),
|
verbose_name=_("Suggested price"),
|
||||||
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
|
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
|
||||||
"value, but not lower than the price this product would have without the free price option."),
|
"value, but not lower than the price this product would have without the free price option. This "
|
||||||
|
"will be ignored if a voucher is used that lowers the price."),
|
||||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||||
)
|
)
|
||||||
tax_rule = models.ForeignKey(
|
tax_rule = models.ForeignKey(
|
||||||
@@ -782,7 +794,7 @@ class Item(LoggedModel):
|
|||||||
return t
|
return t
|
||||||
|
|
||||||
def is_available_by_time(self, now_dt: datetime=None) -> bool:
|
def is_available_by_time(self, now_dt: datetime=None) -> bool:
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
if self.available_from and self.available_from > now_dt:
|
if self.available_from and self.available_from > now_dt:
|
||||||
return False
|
return False
|
||||||
if self.available_until and self.available_until < now_dt:
|
if self.available_until and self.available_until < now_dt:
|
||||||
@@ -794,13 +806,13 @@ class Item(LoggedModel):
|
|||||||
Returns whether this item is available according to its ``active`` flag
|
Returns whether this item is available according to its ``active`` flag
|
||||||
and its ``available_from`` and ``available_until`` fields
|
and its ``available_from`` and ``available_until`` fields
|
||||||
"""
|
"""
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
if not self.active or not self.is_available_by_time(now_dt):
|
if not self.active or not self.is_available_by_time(now_dt):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
subevent_item = subevent and subevent.item_overrides.get(self.pk)
|
||||||
if not self.active:
|
if not self.active:
|
||||||
return 'active'
|
return 'active'
|
||||||
@@ -957,11 +969,11 @@ class Item(LoggedModel):
|
|||||||
return self.validity_fixed_from, self.validity_fixed_until
|
return self.validity_fixed_from, self.validity_fixed_until
|
||||||
elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC:
|
elif self.validity_mode == Item.VALIDITY_MODE_DYNAMIC:
|
||||||
tz = override_tz or self.event.timezone
|
tz = override_tz or self.event.timezone
|
||||||
requested_start = requested_start or now()
|
requested_start = requested_start or time_machine_now()
|
||||||
if enforce_start_limit and not self.validity_dynamic_start_choice:
|
if enforce_start_limit and not self.validity_dynamic_start_choice:
|
||||||
requested_start = now()
|
requested_start = time_machine_now()
|
||||||
if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None:
|
if enforce_start_limit and self.validity_dynamic_start_choice_day_limit is not None:
|
||||||
requested_start = min(requested_start, now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
|
requested_start = min(requested_start, time_machine_now() + timedelta(days=self.validity_dynamic_start_choice_day_limit))
|
||||||
|
|
||||||
valid_until = requested_start.astimezone(tz)
|
valid_until = requested_start.astimezone(tz)
|
||||||
|
|
||||||
@@ -1085,7 +1097,8 @@ class ItemVariation(models.Model):
|
|||||||
free_price_suggestion = models.DecimalField(
|
free_price_suggestion = models.DecimalField(
|
||||||
verbose_name=_("Suggested price"),
|
verbose_name=_("Suggested price"),
|
||||||
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
|
help_text=_("This price will be used as the default value of the input field. The user can choose a lower "
|
||||||
"value, but not lower than the price this product would have without the free price option."),
|
"value, but not lower than the price this product would have without the free price option. This "
|
||||||
|
"will be ignored if a voucher is used that lowers the price."),
|
||||||
max_digits=13, decimal_places=2, null=True, blank=True,
|
max_digits=13, decimal_places=2, null=True, blank=True,
|
||||||
)
|
)
|
||||||
require_approval = models.BooleanField(
|
require_approval = models.BooleanField(
|
||||||
@@ -1290,7 +1303,7 @@ class ItemVariation(models.Model):
|
|||||||
return ItemVariation.objects.filter(item=self.item).count() == 1
|
return ItemVariation.objects.filter(item=self.item).count() == 1
|
||||||
|
|
||||||
def is_available_by_time(self, now_dt: datetime=None) -> bool:
|
def is_available_by_time(self, now_dt: datetime=None) -> bool:
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
if self.available_from and self.available_from > now_dt:
|
if self.available_from and self.available_from > now_dt:
|
||||||
return False
|
return False
|
||||||
if self.available_until and self.available_until < now_dt:
|
if self.available_until and self.available_until < now_dt:
|
||||||
@@ -1302,13 +1315,13 @@ class ItemVariation(models.Model):
|
|||||||
Returns whether this item is available according to its ``active`` flag
|
Returns whether this item is available according to its ``active`` flag
|
||||||
and its ``available_from`` and ``available_until`` fields
|
and its ``available_from`` and ``available_until`` fields
|
||||||
"""
|
"""
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
if not self.active or not self.is_available_by_time(now_dt):
|
if not self.active or not self.is_available_by_time(now_dt):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
def unavailability_reason(self, now_dt: datetime=None, has_voucher=False, subevent=None) -> Optional[str]:
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
subevent_var = subevent and subevent.var_overrides.get(self.pk)
|
||||||
if not self.active:
|
if not self.active:
|
||||||
return 'active'
|
return 'active'
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ from django.db import models
|
|||||||
from django.db.models import Count, OuterRef, Subquery, Value
|
from django.db.models import Count, OuterRef, Subquery, Value
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_scopes import ScopedManager, scopes_disabled
|
from django_scopes import ScopedManager, scopes_disabled
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
@@ -31,6 +30,7 @@ from i18nfield.fields import I18nCharField
|
|||||||
from pretix.base.models import Customer
|
from pretix.base.models import Customer
|
||||||
from pretix.base.models.base import LoggedModel
|
from pretix.base.models.base import LoggedModel
|
||||||
from pretix.base.models.organizer import Organizer
|
from pretix.base.models.organizer import Organizer
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.helpers.names import build_name
|
from pretix.helpers.names import build_name
|
||||||
|
|
||||||
|
|
||||||
@@ -165,13 +165,13 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
def is_valid(self, ev=None, ticket_valid_from=None, valid_from_not_chosen=False):
|
||||||
if valid_from_not_chosen:
|
if valid_from_not_chosen:
|
||||||
return not self.canceled and self.date_end >= now()
|
return not self.canceled and self.date_end >= time_machine_now()
|
||||||
elif ticket_valid_from:
|
elif ticket_valid_from:
|
||||||
dt = ticket_valid_from
|
dt = ticket_valid_from
|
||||||
elif ev:
|
elif ev:
|
||||||
dt = ev.date_from
|
dt = ev.date_from
|
||||||
else:
|
else:
|
||||||
dt = now()
|
dt = time_machine_now()
|
||||||
|
|
||||||
return not self.canceled and dt >= self.date_start and dt <= self.date_end
|
return not self.canceled and dt >= self.date_start and dt <= self.date_end
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
import copy
|
import copy
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import hmac
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
@@ -59,7 +60,7 @@ from django.db.models.functions import Coalesce, Greatest
|
|||||||
from django.db.models.signals import post_delete
|
from django.db.models.signals import post_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string, salted_hmac
|
||||||
from django.utils.encoding import escape_uri_path
|
from django.utils.encoding import escape_uri_path
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
@@ -80,6 +81,7 @@ from pretix.base.models import Customer, User
|
|||||||
from pretix.base.reldate import RelativeDateWrapper
|
from pretix.base.reldate import RelativeDateWrapper
|
||||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||||
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
|
from pretix.base.signals import allow_ticket_download, order_gracefully_delete
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
|
|
||||||
from ...helpers import OF_SELF
|
from ...helpers import OF_SELF
|
||||||
from ...helpers.countries import CachedCountries, FastCountryField
|
from ...helpers.countries import CachedCountries, FastCountryField
|
||||||
@@ -104,6 +106,34 @@ def generate_position_secret():
|
|||||||
raise TypeError("Function no longer exists, use secret generators")
|
raise TypeError("Function no longer exists, use secret generators")
|
||||||
|
|
||||||
|
|
||||||
|
class OrderQuerySet(models.QuerySet):
|
||||||
|
def get_with_secret_check(self, code, received_secret, tag, secret_length=64):
|
||||||
|
dummy = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"[:secret_length]
|
||||||
|
try:
|
||||||
|
order = self.get(code=code)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
# Do a hash comparison as well to harden against timing attacks
|
||||||
|
hmac.compare_digest(
|
||||||
|
salted_hmac(key_salt=b"", value=tag, algorithm="sha256",
|
||||||
|
secret=dummy).hexdigest()[:secret_length],
|
||||||
|
received_secret[:secret_length]
|
||||||
|
)
|
||||||
|
raise Order.DoesNotExist
|
||||||
|
|
||||||
|
if not hmac.compare_digest(
|
||||||
|
order.tagged_secret(tag, secret_length) if tag else order.secret,
|
||||||
|
received_secret[:secret_length].lower() if tag else received_secret.lower()
|
||||||
|
) and not (
|
||||||
|
# TODO: remove this clause after a while (compatibility with old secrets currently in flight)
|
||||||
|
tag and hmac.compare_digest(
|
||||||
|
hashlib.sha1(order.secret.lower().encode()).hexdigest(),
|
||||||
|
received_secret.lower()
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise Order.DoesNotExist
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
class Order(LockModel, LoggedModel):
|
class Order(LockModel, LoggedModel):
|
||||||
"""
|
"""
|
||||||
An order is created when a user clicks 'buy' on his cart. It holds
|
An order is created when a user clicks 'buy' on his cart. It holds
|
||||||
@@ -222,6 +252,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
verbose_name=_('Locale')
|
verbose_name=_('Locale')
|
||||||
)
|
)
|
||||||
secret = models.CharField(max_length=32, default=generate_secret)
|
secret = models.CharField(max_length=32, default=generate_secret)
|
||||||
|
internal_secret = models.CharField(null=True, blank=True, max_length=32, default=generate_secret)
|
||||||
datetime = models.DateTimeField(
|
datetime = models.DateTimeField(
|
||||||
verbose_name=_("Date"), db_index=False
|
verbose_name=_("Date"), db_index=False
|
||||||
)
|
)
|
||||||
@@ -284,7 +315,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ScopedManager(organizer='event__organizer')
|
objects = ScopedManager(OrderQuerySet.as_manager().__class__, organizer='event__organizer')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Order")
|
verbose_name = _("Order")
|
||||||
@@ -681,7 +712,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
for op in positions:
|
for op in positions:
|
||||||
if op.issued_gift_cards.all():
|
if op.issued_gift_cards.all():
|
||||||
return False
|
return False
|
||||||
if self.user_change_deadline and now() > self.user_change_deadline:
|
if self.user_change_deadline and time_machine_now() > self.user_change_deadline:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -713,7 +744,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
return False
|
return False
|
||||||
if op.granted_memberships.with_usages().filter(usages__gt=0):
|
if op.granted_memberships.with_usages().filter(usages__gt=0):
|
||||||
return False
|
return False
|
||||||
if self.user_cancel_deadline and now() > self.user_cancel_deadline:
|
if self.user_cancel_deadline and time_machine_now() > self.user_cancel_deadline:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.status == Order.STATUS_PAID:
|
if self.status == Order.STATUS_PAID:
|
||||||
@@ -850,8 +881,11 @@ class Order(LockModel, LoggedModel):
|
|||||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self.event.settings.allow_modifications not in ("order", "attendee"):
|
||||||
|
return False
|
||||||
|
|
||||||
modify_deadline = self.modify_deadline
|
modify_deadline = self.modify_deadline
|
||||||
if modify_deadline is not None and now() > modify_deadline:
|
if modify_deadline is not None and time_machine_now() > modify_deadline:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
positions = list(
|
positions = list(
|
||||||
@@ -903,7 +937,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
return self.event.settings.ticket_download and (
|
return self.event.settings.ticket_download and (
|
||||||
self.event.settings.ticket_download_date is None
|
self.event.settings.ticket_download_date is None
|
||||||
or self.ticket_download_date is None
|
or self.ticket_download_date is None
|
||||||
or now() > self.ticket_download_date
|
or time_machine_now() > self.ticket_download_date
|
||||||
) and (
|
) and (
|
||||||
self.status == Order.STATUS_PAID
|
self.status == Order.STATUS_PAID
|
||||||
or (
|
or (
|
||||||
@@ -975,7 +1009,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
return error_messages['require_approval']
|
return error_messages['require_approval']
|
||||||
term_last = self.payment_term_last
|
term_last = self.payment_term_last
|
||||||
if term_last and not ignore_date:
|
if term_last and not ignore_date:
|
||||||
if now() > term_last:
|
if time_machine_now() > term_last:
|
||||||
return error_messages['late_lastdate']
|
return error_messages['late_lastdate']
|
||||||
|
|
||||||
if self.status == self.STATUS_PENDING:
|
if self.status == self.STATUS_PENDING:
|
||||||
@@ -998,7 +1032,7 @@ class Order(LockModel, LoggedModel):
|
|||||||
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
'voucher_budget': _('The voucher "{voucher}" no longer has sufficient budget.'),
|
||||||
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
'voucher_usages': _('The voucher "{voucher}" has been used in the meantime.'),
|
||||||
}
|
}
|
||||||
now_dt = now_dt or now()
|
now_dt = now_dt or time_machine_now()
|
||||||
positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher'))
|
positions = list(self.positions.all().select_related('item', 'variation', 'seat', 'voucher'))
|
||||||
quota_cache = {}
|
quota_cache = {}
|
||||||
v_budget = {}
|
v_budget = {}
|
||||||
@@ -1219,6 +1253,10 @@ class Order(LockModel, LoggedModel):
|
|||||||
_transactions_mark_order_clean(self.pk)
|
_transactions_mark_order_clean(self.pk)
|
||||||
return create
|
return create
|
||||||
|
|
||||||
|
def tagged_secret(self, tag, secret_length=64):
|
||||||
|
return salted_hmac(value=tag, key_salt=b"", algorithm="sha256",
|
||||||
|
secret=self.internal_secret or self.secret).hexdigest()[:secret_length]
|
||||||
|
|
||||||
|
|
||||||
def answerfile_name(instance, filename: str) -> str:
|
def answerfile_name(instance, filename: str) -> str:
|
||||||
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
secret = get_random_string(length=32, allowed_chars=string.ascii_letters + string.digits)
|
||||||
@@ -2513,6 +2551,43 @@ class OrderPosition(AbstractPosition):
|
|||||||
reasons[b] = b
|
reasons[b] = b
|
||||||
return reasons
|
return reasons
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_modify_answers(self) -> bool:
|
||||||
|
"""
|
||||||
|
``True`` if the user can change the question answers / attendee names that are
|
||||||
|
related to the position. This checks order status and modification deadlines. It also
|
||||||
|
returns ``False`` if there are no questions that can be answered.
|
||||||
|
"""
|
||||||
|
from .checkin import Checkin
|
||||||
|
|
||||||
|
if self.order.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.event.settings.allow_modifications != "attendee":
|
||||||
|
return False
|
||||||
|
|
||||||
|
modify_deadline = self.order.modify_deadline
|
||||||
|
if modify_deadline is not None and now() > modify_deadline:
|
||||||
|
return False
|
||||||
|
|
||||||
|
positions = list(
|
||||||
|
self.order.positions.all().annotate(
|
||||||
|
has_checkin=Exists(Checkin.objects.filter(position_id=OuterRef('pk'), list__consider_tickets_used=True))
|
||||||
|
).select_related('item').prefetch_related('item__questions')
|
||||||
|
)
|
||||||
|
if not self.event.settings.allow_modifications_after_checkin:
|
||||||
|
for cp in positions:
|
||||||
|
if cp.has_checkin:
|
||||||
|
return False
|
||||||
|
|
||||||
|
ask_names = self.event.settings.get('attendee_names_asked', as_type=bool)
|
||||||
|
for cp in positions:
|
||||||
|
if cp.pk == self.pk or cp.addon_to_id == self.pk:
|
||||||
|
if (cp.item.ask_attendee_data and ask_names) or cp.item.questions.all():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False # nothing there to modify
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def transform_cart_positions(cls, cp: List, order) -> list:
|
def transform_cart_positions(cls, cp: List, order) -> list:
|
||||||
from . import Voucher
|
from . import Voucher
|
||||||
@@ -2535,9 +2610,9 @@ class OrderPosition(AbstractPosition):
|
|||||||
if cartpos.item.validity_mode:
|
if cartpos.item.validity_mode:
|
||||||
valid_from, valid_until = cartpos.item.compute_validity(
|
valid_from, valid_until = cartpos.item.compute_validity(
|
||||||
requested_start=(
|
requested_start=(
|
||||||
max(cartpos.requested_valid_from, now())
|
max(cartpos.requested_valid_from, time_machine_now())
|
||||||
if cartpos.requested_valid_from and cartpos.item.validity_dynamic_start_choice
|
if cartpos.requested_valid_from and cartpos.item.validity_dynamic_start_choice
|
||||||
else now()
|
else time_machine_now()
|
||||||
),
|
),
|
||||||
enforce_start_limit=True,
|
enforce_start_limit=True,
|
||||||
override_tz=order.event.timezone,
|
override_tz=order.event.timezone,
|
||||||
@@ -3063,9 +3138,9 @@ class CartPosition(AbstractPosition):
|
|||||||
def predicted_validity(self):
|
def predicted_validity(self):
|
||||||
return self.item.compute_validity(
|
return self.item.compute_validity(
|
||||||
requested_start=(
|
requested_start=(
|
||||||
max(self.requested_valid_from, now())
|
max(self.requested_valid_from, time_machine_now())
|
||||||
if self.requested_valid_from and self.item.validity_dynamic_start_choice
|
if self.requested_valid_from and self.item.validity_dynamic_start_choice
|
||||||
else now()
|
else time_machine_now()
|
||||||
),
|
),
|
||||||
override_tz=self.event.timezone,
|
override_tz=self.event.timezone,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -370,10 +370,11 @@ class Voucher(LoggedModel):
|
|||||||
'redeemed': redeemed
|
'redeemed': redeemed
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if data.get('max_usages', 1) < data.get('min_usages', 1):
|
if data.get('min_usages') is not None:
|
||||||
raise ValidationError(
|
if data.get('max_usages', 1) < data.get('min_usages', 1):
|
||||||
_('The maximum number of usages may not be lower than the minimum number of usages.'),
|
raise ValidationError(
|
||||||
)
|
_('The maximum number of usages may not be lower than the minimum number of usages.'),
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def clean_subevent(data, event):
|
def clean_subevent(data, event):
|
||||||
@@ -459,7 +460,7 @@ class Voucher(LoggedModel):
|
|||||||
new_quotas = set(
|
new_quotas = set(
|
||||||
Quota.objects.filter(
|
Quota.objects.filter(
|
||||||
pk__in=Quota.variations.through.objects.filter(
|
pk__in=Quota.variations.through.objects.filter(
|
||||||
itemvariation__item=old_instance.item,
|
itemvariation__item=item,
|
||||||
quota__subevent=data.get('subevent'),
|
quota__subevent=data.get('subevent'),
|
||||||
).values('quota_id')
|
).values('quota_id')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ from pretix.base.settings import SettingsSandbox
|
|||||||
from pretix.base.signals import register_payment_providers
|
from pretix.base.signals import register_payment_providers
|
||||||
from pretix.base.templatetags.money import money_filter
|
from pretix.base.templatetags.money import money_filter
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.countries import CachedCountries
|
from pretix.helpers.countries import CachedCountries
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import format_map
|
||||||
@@ -1441,7 +1442,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
if not gc.testmode and self.event.testmode:
|
if not gc.testmode and self.event.testmode:
|
||||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
messages.error(request, _("Only test gift cards can be used in test mode."))
|
||||||
return
|
return
|
||||||
if gc.expires and gc.expires < now():
|
if gc.expires and gc.expires < time_machine_now():
|
||||||
messages.error(request, _("This gift card is no longer valid."))
|
messages.error(request, _("This gift card is no longer valid."))
|
||||||
return
|
return
|
||||||
if gc.value <= Decimal("0.00"):
|
if gc.value <= Decimal("0.00"):
|
||||||
@@ -1491,7 +1492,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
if not gc.testmode and payment.order.testmode:
|
if not gc.testmode and payment.order.testmode:
|
||||||
messages.error(request, _("Only test gift cards can be used in test mode."))
|
messages.error(request, _("Only test gift cards can be used in test mode."))
|
||||||
return
|
return
|
||||||
if gc.expires and gc.expires < now():
|
if gc.expires and gc.expires < time_machine_now():
|
||||||
messages.error(request, _("This gift card is no longer valid."))
|
messages.error(request, _("This gift card is no longer valid."))
|
||||||
return
|
return
|
||||||
if gc.value <= Decimal("0.00"):
|
if gc.value <= Decimal("0.00"):
|
||||||
@@ -1539,7 +1540,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
raise PaymentException(_("This gift card can only be used in test mode."))
|
raise PaymentException(_("This gift card can only be used in test mode."))
|
||||||
if not gc.testmode and payment.order.testmode:
|
if not gc.testmode and payment.order.testmode:
|
||||||
raise PaymentException(_("Only test gift cards can be used in test mode."))
|
raise PaymentException(_("Only test gift cards can be used in test mode."))
|
||||||
if gc.expires and gc.expires < now():
|
if gc.expires and gc.expires < time_machine_now():
|
||||||
raise PaymentException(_("This gift card is no longer valid."))
|
raise PaymentException(_("This gift card is no longer valid."))
|
||||||
|
|
||||||
trans = gc.transactions.create(
|
trans = gc.transactions.create(
|
||||||
|
|||||||
+144
-84
@@ -62,8 +62,7 @@ from django.utils.html import conditional_escape
|
|||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext
|
from django.utils.translation import gettext_lazy as _, pgettext
|
||||||
from i18nfield.strings import LazyI18nString
|
from i18nfield.strings import LazyI18nString
|
||||||
from pypdf import PdfReader, PdfWriter, Transformation
|
from pypdf import PdfReader, PdfWriter
|
||||||
from pypdf.generic import RectangleObject
|
|
||||||
from reportlab.graphics import renderPDF
|
from reportlab.graphics import renderPDF
|
||||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||||
from reportlab.graphics.shapes import Drawing
|
from reportlab.graphics.shapes import Drawing
|
||||||
@@ -408,6 +407,30 @@ DEFAULT_VARIABLES = OrderedDict((
|
|||||||
"TIME_FORMAT"
|
"TIME_FORMAT"
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
("purchase_date", {
|
||||||
|
"label": _("Purchase date"),
|
||||||
|
"editor_sample": _("2017-05-31"),
|
||||||
|
"evaluate": lambda op, order, ev: date_format(
|
||||||
|
order.datetime.astimezone(ev.timezone),
|
||||||
|
"SHORT_DATE_FORMAT"
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
("purchase_datetime", {
|
||||||
|
"label": _("Purchase date and time"),
|
||||||
|
"editor_sample": _("2017-05-31 19:00"),
|
||||||
|
"evaluate": lambda op, order, ev: date_format(
|
||||||
|
order.datetime.astimezone(ev.timezone),
|
||||||
|
"SHORT_DATETIME_FORMAT"
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
("purchase_time", {
|
||||||
|
"label": _("Purchase time"),
|
||||||
|
"editor_sample": _("19:00"),
|
||||||
|
"evaluate": lambda op, order, ev: date_format(
|
||||||
|
order.datetime.astimezone(ev.timezone),
|
||||||
|
"TIME_FORMAT"
|
||||||
|
)
|
||||||
|
}),
|
||||||
("valid_from_date", {
|
("valid_from_date", {
|
||||||
"label": _("Validity start date"),
|
"label": _("Validity start date"),
|
||||||
"editor_sample": _("2017-05-31"),
|
"editor_sample": _("2017-05-31"),
|
||||||
@@ -1045,56 +1068,81 @@ class Renderer:
|
|||||||
canvas.showPage()
|
canvas.showPage()
|
||||||
|
|
||||||
def render_background(self, buffer, title=_('Ticket')):
|
def render_background(self, buffer, title=_('Ticket')):
|
||||||
|
buffer.seek(0)
|
||||||
|
fg_pdf = PdfReader(buffer)
|
||||||
|
|
||||||
if settings.PDFTK:
|
if settings.PDFTK:
|
||||||
buffer.seek(0)
|
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
with open(os.path.join(d, 'back.pdf'), 'wb') as f:
|
fg_filename = os.path.join(d, 'fg.pdf')
|
||||||
f.write(self.bg_bytes)
|
bg_filename = os.path.join(d, 'bg.pdf')
|
||||||
with open(os.path.join(d, 'front.pdf'), 'wb') as f:
|
out_filename = os.path.join(d, 'out.pdf')
|
||||||
|
|
||||||
|
buffer.seek(0)
|
||||||
|
with open(fg_filename, 'wb') as f:
|
||||||
f.write(buffer.read())
|
f.write(buffer.read())
|
||||||
subprocess.run([
|
# pdf_header is a string like "%pdf-X.X"
|
||||||
settings.PDFTK,
|
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||||
os.path.join(d, 'front.pdf'),
|
# To fix issues with pdftk and background-PDF using pdf-version greater
|
||||||
'multibackground',
|
# than foreground-PDF, we stamp front onto back instead.
|
||||||
os.path.join(d, 'back.pdf'),
|
# Just changing PDF-version in fg.pdf to match the version of
|
||||||
'output',
|
# bg.pdf as we do with pypdf, does not work with pdftk.
|
||||||
os.path.join(d, 'out.pdf'),
|
#
|
||||||
'compress'
|
# Make sure that bg.pdf matches the number of pages of fg.pdf
|
||||||
], check=True)
|
# note: self.bg_pdf is a PdfReader(), not a PdfWriter()
|
||||||
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
|
fg_num_pages = fg_pdf.get_num_pages()
|
||||||
|
bg_num_pages = self.bg_pdf.get_num_pages()
|
||||||
|
bg_pdf_to_merge = PdfWriter()
|
||||||
|
bg_pdf_to_merge.append(
|
||||||
|
self.bg_pdf,
|
||||||
|
pages=(0, min(bg_num_pages, fg_num_pages)),
|
||||||
|
import_outline=False,
|
||||||
|
excluded_fields=("/Annots", "/B")
|
||||||
|
)
|
||||||
|
if fg_num_pages > bg_num_pages:
|
||||||
|
# repeat last page in bg_pdf to match fg_pdf
|
||||||
|
bg_pdf_to_merge.append(
|
||||||
|
bg_pdf_to_merge,
|
||||||
|
pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages),
|
||||||
|
import_outline=False,
|
||||||
|
excluded_fields=("/Annots", "/B")
|
||||||
|
)
|
||||||
|
|
||||||
|
bg_pdf_to_merge.write(bg_filename)
|
||||||
|
|
||||||
|
pdftk_cmd = [
|
||||||
|
settings.PDFTK,
|
||||||
|
bg_filename,
|
||||||
|
'multistamp',
|
||||||
|
fg_filename
|
||||||
|
]
|
||||||
|
|
||||||
|
else:
|
||||||
|
with open(bg_filename, 'wb') as f:
|
||||||
|
f.write(self.bg_bytes)
|
||||||
|
pdftk_cmd = [
|
||||||
|
settings.PDFTK,
|
||||||
|
fg_filename,
|
||||||
|
'multibackground',
|
||||||
|
bg_filename
|
||||||
|
]
|
||||||
|
|
||||||
|
pdftk_cmd.extend(('output', out_filename, 'compress'))
|
||||||
|
subprocess.run(pdftk_cmd, check=True)
|
||||||
|
with open(out_filename, 'rb') as f:
|
||||||
return BytesIO(f.read())
|
return BytesIO(f.read())
|
||||||
else:
|
else:
|
||||||
buffer.seek(0)
|
|
||||||
new_pdf = PdfReader(buffer)
|
|
||||||
output = PdfWriter()
|
output = PdfWriter()
|
||||||
|
|
||||||
for i, page in enumerate(new_pdf.pages):
|
for i, page in enumerate(fg_pdf.pages):
|
||||||
bg_page = copy.deepcopy(self.bg_pdf.pages[i])
|
bg_page = self.bg_pdf.pages[i]
|
||||||
bg_rotation = bg_page.get('/Rotate')
|
if bg_page.rotation != 0:
|
||||||
if bg_rotation:
|
bg_page.transfer_rotation_to_content()
|
||||||
# /Rotate is clockwise, transformation.rotate is counter-clockwise
|
page.merge_page(bg_page, over=False)
|
||||||
t = Transformation().rotate(bg_rotation)
|
output.add_page(page)
|
||||||
w = float(page.mediabox.getWidth())
|
|
||||||
h = float(page.mediabox.getHeight())
|
# pdf_header is a string like "%pdf-X.X"
|
||||||
if bg_rotation in (90, 270):
|
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||||
# offset due to rotation base
|
output.pdf_header = self.bg_pdf.pdf_header
|
||||||
if bg_rotation == 90:
|
|
||||||
t = t.translate(h, 0)
|
|
||||||
else:
|
|
||||||
t = t.translate(0, w)
|
|
||||||
# rotate mediabox as well
|
|
||||||
page.mediabox = RectangleObject((
|
|
||||||
page.mediabox.left.as_numeric(),
|
|
||||||
page.mediabox.bottom.as_numeric(),
|
|
||||||
page.mediabox.top.as_numeric(),
|
|
||||||
page.mediabox.right.as_numeric(),
|
|
||||||
))
|
|
||||||
page.trimbox = page.mediabox
|
|
||||||
elif bg_rotation == 180:
|
|
||||||
t = t.translate(w, h)
|
|
||||||
page.add_transformation(t)
|
|
||||||
bg_page.merge_page(page)
|
|
||||||
output.add_page(bg_page)
|
|
||||||
|
|
||||||
output.add_metadata({
|
output.add_metadata({
|
||||||
'/Title': str(title),
|
'/Title': str(title),
|
||||||
@@ -1106,54 +1154,66 @@ class Renderer:
|
|||||||
return outbuffer
|
return outbuffer
|
||||||
|
|
||||||
|
|
||||||
def merge_background(fg_pdf, bg_pdf, out_file, compress):
|
def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
|
||||||
if settings.PDFTK:
|
if settings.PDFTK:
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
fg_filename = os.path.join(d, 'fg.pdf')
|
fg_filename = os.path.join(d, 'fg.pdf')
|
||||||
bg_filename = os.path.join(d, 'bg.pdf')
|
bg_filename = os.path.join(d, 'bg.pdf')
|
||||||
fg_pdf.write(fg_filename)
|
|
||||||
bg_pdf.write(bg_filename)
|
# pdf_header is a string like "%pdf-X.X"
|
||||||
pdftk_cmd = [
|
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||||
settings.PDFTK,
|
# To fix issues with pdftk and background-PDF using pdf-version greater
|
||||||
fg_filename,
|
# than foreground-PDF, we stamp front onto back instead.
|
||||||
'multibackground',
|
# Just changing PDF-version in fg.pdf to match the version of
|
||||||
bg_filename,
|
# bg.pdf as we do with pypdf, does not work with pdftk.
|
||||||
'output',
|
|
||||||
'-',
|
# Make sure that bg.pdf matches the number of pages of fg.pdf
|
||||||
]
|
fg_num_pages = fg_pdf.get_num_pages()
|
||||||
|
bg_num_pages = bg_pdf.get_num_pages()
|
||||||
|
if fg_num_pages > bg_num_pages:
|
||||||
|
# repeat last page in bg_pdf to match fg_pdf
|
||||||
|
bg_pdf.append(
|
||||||
|
bg_pdf,
|
||||||
|
pages=[bg_num_pages - 1] * (fg_num_pages - bg_num_pages),
|
||||||
|
import_outline=False,
|
||||||
|
excluded_fields=("/Annots", "/B")
|
||||||
|
)
|
||||||
|
|
||||||
|
bg_pdf.write(bg_filename)
|
||||||
|
|
||||||
|
pdftk_cmd = [
|
||||||
|
settings.PDFTK,
|
||||||
|
bg_filename,
|
||||||
|
'multistamp',
|
||||||
|
fg_filename,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
pdftk_cmd = [
|
||||||
|
settings.PDFTK,
|
||||||
|
fg_filename,
|
||||||
|
'multibackground',
|
||||||
|
bg_filename
|
||||||
|
]
|
||||||
|
|
||||||
|
pdftk_cmd.extend(('output', '-'))
|
||||||
if compress:
|
if compress:
|
||||||
pdftk_cmd.append('compress')
|
pdftk_cmd.append('compress')
|
||||||
|
|
||||||
|
fg_pdf.write(fg_filename)
|
||||||
|
bg_pdf.write(bg_filename)
|
||||||
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
||||||
else:
|
else:
|
||||||
output = PdfWriter()
|
|
||||||
for i, page in enumerate(fg_pdf.pages):
|
for i, page in enumerate(fg_pdf.pages):
|
||||||
bg_page = copy.deepcopy(bg_pdf.pages[i])
|
bg_page = bg_pdf.pages[i]
|
||||||
bg_rotation = bg_page.get('/Rotate')
|
if bg_page.rotation != 0:
|
||||||
if bg_rotation:
|
bg_page.transfer_rotation_to_content()
|
||||||
# /Rotate is clockwise, transformation.rotate is counter-clockwise
|
page.merge_page(bg_page, over=False)
|
||||||
t = Transformation().rotate(bg_rotation)
|
|
||||||
w = float(page.mediabox.getWidth())
|
# pdf_header is a string like "%pdf-X.X"
|
||||||
h = float(page.mediabox.getHeight())
|
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||||
if bg_rotation in (90, 270):
|
fg_pdf.pdf_header = bg_pdf.pdf_header
|
||||||
# offset due to rotation base
|
|
||||||
if bg_rotation == 90:
|
fg_pdf.write(out_file)
|
||||||
t = t.translate(h, 0)
|
|
||||||
else:
|
|
||||||
t = t.translate(0, w)
|
|
||||||
# rotate mediabox as well
|
|
||||||
page.mediabox = RectangleObject((
|
|
||||||
page.mediabox.left.as_numeric(),
|
|
||||||
page.mediabox.bottom.as_numeric(),
|
|
||||||
page.mediabox.top.as_numeric(),
|
|
||||||
page.mediabox.right.as_numeric(),
|
|
||||||
))
|
|
||||||
page.trimbox = page.mediabox
|
|
||||||
elif bg_rotation == 180:
|
|
||||||
t = t.translate(w, h)
|
|
||||||
page.add_transformation(t)
|
|
||||||
bg_page.merge_page(page)
|
|
||||||
output.add_page(bg_page)
|
|
||||||
output.write(out_file)
|
|
||||||
|
|
||||||
|
|
||||||
@deconstructible
|
@deconstructible
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ def assign_ticket_secret(event, position, force_invalidate_if_revokation_list_us
|
|||||||
kwargs['valid_from'] = position.valid_from
|
kwargs['valid_from'] = position.valid_from
|
||||||
if 'valid_until' in params:
|
if 'valid_until' in params:
|
||||||
kwargs['valid_until'] = position.valid_until
|
kwargs['valid_until'] = position.valid_until
|
||||||
|
if 'order_datetime' in params:
|
||||||
|
kwargs['order_datetime'] = position.order.datetime
|
||||||
secret = gen.generate_secret(
|
secret = gen.generate_secret(
|
||||||
item=position.item,
|
item=position.item,
|
||||||
variation=position.variation,
|
variation=position.variation,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ from pretix.base.services.tasks import ProfiledEventTask
|
|||||||
from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList
|
from pretix.base.settings import PERSON_NAME_SCHEMES, LazyI18nStringList
|
||||||
from pretix.base.signals import validate_cart_addons
|
from pretix.base.signals import validate_cart_addons
|
||||||
from pretix.base.templatetags.rich_text import rich_text
|
from pretix.base.templatetags.rich_text import rich_text
|
||||||
|
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.presale.signals import (
|
from pretix.presale.signals import (
|
||||||
checkout_confirm_messages, fee_calculation_for_cart,
|
checkout_confirm_messages, fee_calculation_for_cart,
|
||||||
@@ -278,7 +279,7 @@ class CartManager:
|
|||||||
sales_channel='web'):
|
sales_channel='web'):
|
||||||
self.event = event
|
self.event = event
|
||||||
self.cart_id = cart_id
|
self.cart_id = cart_id
|
||||||
self.now_dt = now()
|
self.real_now_dt = now()
|
||||||
self._operations = []
|
self._operations = []
|
||||||
self._quota_diff = Counter()
|
self._quota_diff = Counter()
|
||||||
self._voucher_use_diff = Counter()
|
self._voucher_use_diff = Counter()
|
||||||
@@ -305,10 +306,10 @@ class CartManager:
|
|||||||
return self._seated_cache[item, subevent]
|
return self._seated_cache[item, subevent]
|
||||||
|
|
||||||
def _calculate_expiry(self):
|
def _calculate_expiry(self):
|
||||||
self._expiry = self.now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
self._expiry = self.real_now_dt + timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
||||||
|
|
||||||
def _check_presale_dates(self):
|
def _check_presale_dates(self):
|
||||||
if self.event.presale_start and self.now_dt < self.event.presale_start:
|
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
|
||||||
raise CartError(error_messages['not_started'])
|
raise CartError(error_messages['not_started'])
|
||||||
if self.event.presale_has_ended:
|
if self.event.presale_has_ended:
|
||||||
raise CartError(error_messages['ended'])
|
raise CartError(error_messages['ended'])
|
||||||
@@ -319,13 +320,13 @@ class CartManager:
|
|||||||
tlv.datetime(self.event).date(),
|
tlv.datetime(self.event).date(),
|
||||||
time(hour=23, minute=59, second=59)
|
time(hour=23, minute=59, second=59)
|
||||||
), self.event.timezone)
|
), self.event.timezone)
|
||||||
if term_last < self.now_dt:
|
if term_last < time_machine_now(self.real_now_dt):
|
||||||
raise CartError(error_messages['payment_ended'])
|
raise CartError(error_messages['payment_ended'])
|
||||||
|
|
||||||
def _extend_expiry_of_valid_existing_positions(self):
|
def _extend_expiry_of_valid_existing_positions(self):
|
||||||
# Extend this user's cart session to ensure all items in the cart expire at the same time
|
# Extend this user's cart session to ensure all items in the cart expire at the same time
|
||||||
# We can extend the reservation of items which are not yet expired without risk
|
# We can extend the reservation of items which are not yet expired without risk
|
||||||
self.positions.filter(expires__gt=self.now_dt).update(expires=self._expiry)
|
self.positions.filter(expires__gt=self.real_now_dt).update(expires=self._expiry)
|
||||||
|
|
||||||
def _delete_out_of_timeframe(self):
|
def _delete_out_of_timeframe(self):
|
||||||
err = None
|
err = None
|
||||||
@@ -333,12 +334,12 @@ class CartManager:
|
|||||||
if not cp.pk:
|
if not cp.pk:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_start and self.now_dt < cp.subevent.presale_start:
|
if cp.subevent and cp.subevent.presale_start and time_machine_now(self.real_now_dt) < cp.subevent.presale_start:
|
||||||
err = error_messages['some_subevent_not_started']
|
err = error_messages['some_subevent_not_started']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_end and self.now_dt > cp.subevent.presale_end:
|
if cp.subevent and cp.subevent.presale_end and time_machine_now(self.real_now_dt) > cp.subevent.presale_end:
|
||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
@@ -350,7 +351,7 @@ class CartManager:
|
|||||||
tlv.datetime(cp.subevent).date(),
|
tlv.datetime(cp.subevent).date(),
|
||||||
time(hour=23, minute=59, second=59)
|
time(hour=23, minute=59, second=59)
|
||||||
), self.event.timezone)
|
), self.event.timezone)
|
||||||
if term_last < self.now_dt:
|
if term_last < time_machine_now(self.real_now_dt):
|
||||||
err = error_messages['some_subevent_ended']
|
err = error_messages['some_subevent_ended']
|
||||||
cp.addons.all().delete()
|
cp.addons.all().delete()
|
||||||
cp.delete()
|
cp.delete()
|
||||||
@@ -449,7 +450,7 @@ class CartManager:
|
|||||||
if op.subevent and not op.subevent.active:
|
if op.subevent and not op.subevent.active:
|
||||||
raise CartError(error_messages['inactive_subevent'])
|
raise CartError(error_messages['inactive_subevent'])
|
||||||
|
|
||||||
if op.subevent and op.subevent.presale_start and self.now_dt < op.subevent.presale_start:
|
if op.subevent and op.subevent.presale_start and time_machine_now(self.real_now_dt) < op.subevent.presale_start:
|
||||||
raise CartError(error_messages['not_started'])
|
raise CartError(error_messages['not_started'])
|
||||||
|
|
||||||
if op.subevent and op.subevent.presale_has_ended:
|
if op.subevent and op.subevent.presale_has_ended:
|
||||||
@@ -472,7 +473,7 @@ class CartManager:
|
|||||||
tlv.datetime(op.subevent).date(),
|
tlv.datetime(op.subevent).date(),
|
||||||
time(hour=23, minute=59, second=59)
|
time(hour=23, minute=59, second=59)
|
||||||
), self.event.timezone)
|
), self.event.timezone)
|
||||||
if term_last < self.now_dt:
|
if term_last < time_machine_now(self.real_now_dt):
|
||||||
raise CartError(error_messages['payment_ended'])
|
raise CartError(error_messages['payment_ended'])
|
||||||
|
|
||||||
if isinstance(op, self.AddOperation):
|
if isinstance(op, self.AddOperation):
|
||||||
@@ -509,7 +510,7 @@ class CartManager:
|
|||||||
)
|
)
|
||||||
if not self.event.settings.seating_choice:
|
if not self.event.settings.seating_choice:
|
||||||
requires_seat = Value(0, output_field=IntegerField())
|
requires_seat = Value(0, output_field=IntegerField())
|
||||||
expired = self.positions.filter(expires__lte=self.now_dt).select_related(
|
expired = self.positions.filter(expires__lte=self.real_now_dt).select_related(
|
||||||
'item', 'variation', 'voucher', 'addon_to', 'addon_to__item'
|
'item', 'variation', 'voucher', 'addon_to', 'addon_to__item'
|
||||||
).annotate(
|
).annotate(
|
||||||
requires_seat=requires_seat
|
requires_seat=requires_seat
|
||||||
@@ -690,7 +691,7 @@ class CartManager:
|
|||||||
# than either of the possible default assumptions.
|
# than either of the possible default assumptions.
|
||||||
predicted_redeemed_after = (
|
predicted_redeemed_after = (
|
||||||
voucher.redeemed +
|
voucher.redeemed +
|
||||||
CartPosition.objects.filter(voucher=voucher, expires__gte=self.now_dt).count() +
|
CartPosition.objects.filter(voucher=voucher, expires__gte=self.real_now_dt).count() +
|
||||||
self._voucher_use_diff[voucher] +
|
self._voucher_use_diff[voucher] +
|
||||||
voucher_use_diff[voucher]
|
voucher_use_diff[voucher]
|
||||||
)
|
)
|
||||||
@@ -982,7 +983,7 @@ class CartManager:
|
|||||||
current_num = len(current_addons[cp].get(k, []))
|
current_num = len(current_addons[cp].get(k, []))
|
||||||
if input_num < current_num:
|
if input_num < current_num:
|
||||||
for a in current_addons[cp][k][:current_num - input_num]:
|
for a in current_addons[cp][k][:current_num - input_num]:
|
||||||
if a.expires > self.now_dt:
|
if a.expires > self.real_now_dt:
|
||||||
quotas = list(a.quotas)
|
quotas = list(a.quotas)
|
||||||
|
|
||||||
for quota in quotas:
|
for quota in quotas:
|
||||||
@@ -996,7 +997,7 @@ class CartManager:
|
|||||||
|
|
||||||
def _get_voucher_availability(self):
|
def _get_voucher_availability(self):
|
||||||
vouchers_ok, self._voucher_depend_on_cart = _get_voucher_availability(
|
vouchers_ok, self._voucher_depend_on_cart = _get_voucher_availability(
|
||||||
self.event, self._voucher_use_diff, self.now_dt,
|
self.event, self._voucher_use_diff, self.real_now_dt,
|
||||||
exclude_position_ids=[
|
exclude_position_ids=[
|
||||||
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
|
op.position.id for op in self._operations if isinstance(op, self.ExtendOperation)
|
||||||
]
|
]
|
||||||
@@ -1101,7 +1102,7 @@ class CartManager:
|
|||||||
shared_lock_objects=[self.event]
|
shared_lock_objects=[self.event]
|
||||||
)
|
)
|
||||||
vouchers_ok = self._get_voucher_availability()
|
vouchers_ok = self._get_voucher_availability()
|
||||||
quotas_ok = _get_quota_availability(self._quota_diff, self.now_dt)
|
quotas_ok = _get_quota_availability(self._quota_diff, self.real_now_dt)
|
||||||
err = None
|
err = None
|
||||||
new_cart_positions = []
|
new_cart_positions = []
|
||||||
deleted_positions = set()
|
deleted_positions = set()
|
||||||
@@ -1118,7 +1119,7 @@ class CartManager:
|
|||||||
|
|
||||||
for iop, op in enumerate(self._operations):
|
for iop, op in enumerate(self._operations):
|
||||||
if isinstance(op, self.RemoveOperation):
|
if isinstance(op, self.RemoveOperation):
|
||||||
if op.position.expires > self.now_dt:
|
if op.position.expires > self.real_now_dt:
|
||||||
for q in op.position.quotas:
|
for q in op.position.quotas:
|
||||||
quotas_ok[q] += 1
|
quotas_ok[q] += 1
|
||||||
addons = op.position.addons.all()
|
addons = op.position.addons.all()
|
||||||
@@ -1395,7 +1396,7 @@ class CartManager:
|
|||||||
err = self.extend_expired_positions() or err
|
err = self.extend_expired_positions() or err
|
||||||
err = err or self._check_min_per_voucher()
|
err = err or self._check_min_per_voucher()
|
||||||
|
|
||||||
self.now_dt = now()
|
self.real_now_dt = now()
|
||||||
|
|
||||||
self._extend_expiry_of_valid_existing_positions()
|
self._extend_expiry_of_valid_existing_positions()
|
||||||
err = self._perform_operations() or err
|
err = self._perform_operations() or err
|
||||||
@@ -1487,7 +1488,7 @@ def get_fees(event, request, total, invoice_address, payments, positions):
|
|||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
|
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
|
||||||
invoice_address: int=None, widget_data=None, sales_channel='web') -> None:
|
invoice_address: int=None, widget_data=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a list of items to a user's cart.
|
Adds a list of items to a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
@@ -1495,7 +1496,7 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
|||||||
:param cart_id: Session ID of a guest
|
:param cart_id: Session ID of a guest
|
||||||
:raises CartError: On any error that occurred
|
:raises CartError: On any error that occurred
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
ia = False
|
ia = False
|
||||||
if invoice_address:
|
if invoice_address:
|
||||||
try:
|
try:
|
||||||
@@ -1517,14 +1518,14 @@ def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, lo
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param voucher: A voucher code
|
:param voucher: A voucher code
|
||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||||
@@ -1537,14 +1538,14 @@ def apply_voucher(self, event: Event, voucher: str, cart_id: str=None, locale='e
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
def remove_cart_position(self, event: Event, position: int, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param position: A cart position ID
|
:param position: A cart position ID
|
||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||||
@@ -1557,13 +1558,13 @@ def remove_cart_position(self, event: Event, position: int, cart_id: str=None, l
|
|||||||
|
|
||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web') -> None:
|
def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||||
@@ -1577,14 +1578,14 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
|||||||
|
|
||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||||
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
def set_cart_addons(self, event: Event, addons: List[dict], cart_id: str=None, locale='en',
|
||||||
invoice_address: int=None, sales_channel='web') -> None:
|
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||||
"""
|
"""
|
||||||
Removes a list of items from a user's cart.
|
Removes a list of items from a user's cart.
|
||||||
:param event: The event ID in question
|
:param event: The event ID in question
|
||||||
:param addons: A list of dicts with the keys addon_to, item, variation
|
:param addons: A list of dicts with the keys addon_to, item, variation
|
||||||
:param session: Session ID of a guest
|
:param session: Session ID of a guest
|
||||||
"""
|
"""
|
||||||
with language(locale):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
ia = False
|
ia = False
|
||||||
if invoice_address:
|
if invoice_address:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -25,13 +25,13 @@ from typing import List, Optional
|
|||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.timezone import now
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
|
AbstractPosition, CartPosition, Customer, Event, Item, Membership, Order,
|
||||||
OrderPosition, SubEvent,
|
OrderPosition, SubEvent,
|
||||||
)
|
)
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ def membership_validity(item: Item, subevent: Optional[SubEvent], event: Event):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
# Always start at start of day
|
# Always start at start of day
|
||||||
date_start = now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
date_start = time_machine_now().astimezone(tz).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
date_end = date_start
|
date_end = date_start
|
||||||
|
|
||||||
if item.grant_membership_duration_months:
|
if item.grant_membership_duration_months:
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ def import_vouchers(event: Event, fileid: str, settings: dict, locale: str, user
|
|||||||
voucher = Voucher(event=event)
|
voucher = Voucher(event=event)
|
||||||
vouchers.append(voucher)
|
vouchers.append(voucher)
|
||||||
|
|
||||||
|
if not record.get("code"):
|
||||||
|
raise ValidationError(_('A voucher cannot be created without a code.'))
|
||||||
Voucher.clean_item_properties(
|
Voucher.clean_item_properties(
|
||||||
record,
|
record,
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -99,9 +99,10 @@ from pretix.base.services.quotas import QuotaAvailability
|
|||||||
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
from pretix.base.services.tasks import ProfiledEventTask, ProfiledTask
|
||||||
from pretix.base.signals import (
|
from pretix.base.signals import (
|
||||||
order_approved, order_canceled, order_changed, order_denied, order_expired,
|
order_approved, order_canceled, order_changed, order_denied, order_expired,
|
||||||
order_fee_calculation, order_paid, order_placed, order_split,
|
order_fee_calculation, order_paid, order_placed, order_reactivated,
|
||||||
order_valid_if_pending, periodic_task, validate_order,
|
order_split, order_valid_if_pending, periodic_task, validate_order,
|
||||||
)
|
)
|
||||||
|
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
|
||||||
from pretix.celery_app import app
|
from pretix.celery_app import app
|
||||||
from pretix.helpers import OF_SELF
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.models import modelcopy
|
from pretix.helpers.models import modelcopy
|
||||||
@@ -253,7 +254,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
|||||||
else:
|
else:
|
||||||
raise OrderError(is_available)
|
raise OrderError(is_available)
|
||||||
|
|
||||||
order_approved.send(order.event, order=order)
|
order_reactivated.send(order.event, order=order)
|
||||||
if order.status == Order.STATUS_PAID:
|
if order.status == Order.STATUS_PAID:
|
||||||
order_paid.send(order.event, order=order)
|
order_paid.send(order.event, order=order)
|
||||||
|
|
||||||
@@ -468,10 +469,10 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
|||||||
order_denied.send(order.event, order=order)
|
order_denied.send(order.event, order=order)
|
||||||
|
|
||||||
if send_mail:
|
if send_mail:
|
||||||
email_template = order.event.settings.mail_text_order_denied
|
|
||||||
email_subject = order.event.settings.mail_subject_order_denied
|
|
||||||
email_context = get_email_context(event=order.event, order=order, comment=comment)
|
|
||||||
with language(order.locale, order.event.settings.region):
|
with language(order.locale, order.event.settings.region):
|
||||||
|
email_template = order.event.settings.mail_text_order_denied
|
||||||
|
email_subject = order.event.settings.mail_subject_order_denied
|
||||||
|
email_context = get_email_context(event=order.event, order=order, comment=comment)
|
||||||
try:
|
try:
|
||||||
order.send_mail(
|
order.send_mail(
|
||||||
email_subject, email_template, email_context,
|
email_subject, email_template, email_context,
|
||||||
@@ -648,10 +649,11 @@ def _check_date(event: Event, now_dt: datetime):
|
|||||||
raise OrderError(error_messages['ended'])
|
raise OrderError(error_messages['ended'])
|
||||||
|
|
||||||
|
|
||||||
def _check_positions(event: Event, now_dt: datetime, positions: List[CartPosition], address: InvoiceAddress=None,
|
def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: datetime, positions: List[CartPosition],
|
||||||
|
address: InvoiceAddress = None,
|
||||||
sales_channel='web', customer=None):
|
sales_channel='web', customer=None):
|
||||||
err = None
|
err = None
|
||||||
_check_date(event, now_dt)
|
_check_date(event, time_machine_now_dt)
|
||||||
|
|
||||||
products_seen = Counter()
|
products_seen = Counter()
|
||||||
q_avail = Counter()
|
q_avail = Counter()
|
||||||
@@ -729,7 +731,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.subevent.presale_start and now_dt < cp.subevent.presale_start:
|
if cp.subevent and cp.subevent.presale_start and time_machine_now_dt < cp.subevent.presale_start:
|
||||||
err = err or error_messages['some_subevent_not_started']
|
err = err or error_messages['some_subevent_not_started']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
break
|
break
|
||||||
@@ -741,7 +743,7 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
tlv.datetime(cp.subevent).date(),
|
tlv.datetime(cp.subevent).date(),
|
||||||
time(hour=23, minute=59, second=59)
|
time(hour=23, minute=59, second=59)
|
||||||
), event.timezone)
|
), event.timezone)
|
||||||
if term_last < now_dt:
|
if term_last < time_machine_now_dt:
|
||||||
err = err or error_messages['some_subevent_ended']
|
err = err or error_messages['some_subevent_ended']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
break
|
break
|
||||||
@@ -787,19 +789,19 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
|
|||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(now_dt):
|
if cp.subevent and cp.item.pk in cp.subevent.item_overrides and not cp.subevent.item_overrides[cp.item.pk].is_available(time_machine_now_dt):
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
|
if cp.subevent and cp.variation and cp.variation.pk in cp.subevent.var_overrides and \
|
||||||
not cp.subevent.var_overrides[cp.variation.pk].is_available(now_dt):
|
not cp.subevent.var_overrides[cp.variation.pk].is_available(time_machine_now_dt):
|
||||||
err = err or error_messages['unavailable']
|
err = err or error_messages['unavailable']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if cp.voucher:
|
if cp.voucher:
|
||||||
if cp.voucher.valid_until and cp.voucher.valid_until < now_dt:
|
if cp.voucher.valid_until and cp.voucher.valid_until < time_machine_now_dt:
|
||||||
err = err or error_messages['voucher_expired']
|
err = err or error_messages['voucher_expired']
|
||||||
delete(cp)
|
delete(cp)
|
||||||
continue
|
continue
|
||||||
@@ -1163,7 +1165,8 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
warnings = []
|
warnings = []
|
||||||
any_payment_failed = False
|
any_payment_failed = False
|
||||||
|
|
||||||
now_dt = now()
|
real_now_dt = now()
|
||||||
|
time_machine_now_dt = time_machine_now(real_now_dt)
|
||||||
err_out = None
|
err_out = None
|
||||||
with transaction.atomic(durable=True):
|
with transaction.atomic(durable=True):
|
||||||
positions = list(
|
positions = list(
|
||||||
@@ -1175,14 +1178,15 @@ def _perform_order(event: Event, payment_requests: List[dict], position_ids: Lis
|
|||||||
if len(position_ids) != len(positions):
|
if len(position_ids) != len(positions):
|
||||||
raise OrderError(error_messages['internal'])
|
raise OrderError(error_messages['internal'])
|
||||||
try:
|
try:
|
||||||
_check_positions(event, now_dt, positions, address=addr, sales_channel=sales_channel, customer=customer)
|
_check_positions(event, real_now_dt, time_machine_now_dt, positions,
|
||||||
|
address=addr, sales_channel=sales_channel, customer=customer)
|
||||||
except OrderError as e:
|
except OrderError as e:
|
||||||
err_out = e # Don't raise directly to make sure transaction is committed, as it might have deleted things
|
err_out = e # Don't raise directly to make sure transaction is committed, as it might have deleted things
|
||||||
else:
|
else:
|
||||||
if 'sleep-after-quota-check' in debugflags_var.get():
|
if 'sleep-after-quota-check' in debugflags_var.get():
|
||||||
sleep(2)
|
sleep(2)
|
||||||
|
|
||||||
order, payment_objs = _create_order(event, email, positions, now_dt, payment_requests,
|
order, payment_objs = _create_order(event, email, positions, real_now_dt, payment_requests,
|
||||||
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
locale=locale, address=addr, meta_info=meta_info, sales_channel=sales_channel,
|
||||||
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
shown_total=shown_total, customer=customer, valid_if_pending=valid_if_pending)
|
||||||
|
|
||||||
@@ -2013,6 +2017,20 @@ class OrderChangeManager:
|
|||||||
for a in current_addons[cp][k][:current_num - input_num]:
|
for a in current_addons[cp][k][:current_num - input_num]:
|
||||||
if a.canceled:
|
if a.canceled:
|
||||||
continue
|
continue
|
||||||
|
is_unavailable = (
|
||||||
|
# If an item is no longer available due to time, it should usually also be no longer
|
||||||
|
# user-removable, because e.g. the stock has already been ordered.
|
||||||
|
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
|
||||||
|
# not mean it should be unremovable for others.
|
||||||
|
# This also prevents accidental removal through the UI because a hidden product will no longer
|
||||||
|
# be part of the input.
|
||||||
|
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
|
||||||
|
or (a.variation and self.order.sales_channel not in a.variation.sales_channels)
|
||||||
|
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
|
||||||
|
or self.order.sales_channel not in item.sales_channels
|
||||||
|
)
|
||||||
|
if is_unavailable:
|
||||||
|
continue
|
||||||
if a.checkins.filter(list__consider_tickets_used=True).exists():
|
if a.checkins.filter(list__consider_tickets_used=True).exists():
|
||||||
raise OrderError(
|
raise OrderError(
|
||||||
error_messages['addon_already_checked_in'] % {
|
error_messages['addon_already_checked_in'] % {
|
||||||
@@ -2835,8 +2853,8 @@ class OrderChangeManager:
|
|||||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||||
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
def perform_order(self, event: Event, payments: List[dict], positions: List[str],
|
||||||
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
email: str=None, locale: str=None, address: int=None, meta_info: dict=None,
|
||||||
sales_channel: str='web', shown_total=None, customer=None):
|
sales_channel: str='web', shown_total=None, customer=None, override_now_dt: datetime=None):
|
||||||
with language(locale):
|
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
return _perform_order(event, payments, positions, email, locale, address, meta_info,
|
||||||
|
|||||||
@@ -337,6 +337,40 @@ def base_placeholders(sender, **kwargs):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_info_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.modify', kwargs={
|
||||||
|
'order': position.order.code,
|
||||||
|
'secret': position.web_secret,
|
||||||
|
'position': position.positionid
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.modify', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'position': '123',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SimpleFunctionalTextPlaceholder(
|
||||||
|
'url_products_change', ['position', 'event'], lambda position, event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.change', kwargs={
|
||||||
|
'order': position.order.code,
|
||||||
|
'secret': position.web_secret,
|
||||||
|
'position': position.positionid
|
||||||
|
}
|
||||||
|
), lambda event: build_absolute_uri(
|
||||||
|
event,
|
||||||
|
'presale:event.order.position.change', kwargs={
|
||||||
|
'order': 'F8VVL',
|
||||||
|
'secret': '6zzjnumtsx136ddy',
|
||||||
|
'position': '123'
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
SimpleFunctionalTextPlaceholder(
|
SimpleFunctionalTextPlaceholder(
|
||||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||||
lambda order, event:
|
lambda order, event:
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ from typing import List, Optional, Tuple
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.timezone import now
|
|
||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.models import (
|
from pretix.base.models import (
|
||||||
@@ -33,6 +32,7 @@ from pretix.base.models import (
|
|||||||
)
|
)
|
||||||
from pretix.base.models.event import Event, SubEvent
|
from pretix.base.models.event import Event, SubEvent
|
||||||
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
|
||||||
|
from pretix.base.timemachine import time_machine_now
|
||||||
|
|
||||||
|
|
||||||
def get_price(item: Item, variation: ItemVariation = None,
|
def get_price(item: Item, variation: ItemVariation = None,
|
||||||
@@ -167,8 +167,8 @@ def apply_discounts(event: Event, sales_channel: str,
|
|||||||
new_prices = {}
|
new_prices = {}
|
||||||
|
|
||||||
discount_qs = event.discounts.filter(
|
discount_qs = event.discounts.filter(
|
||||||
Q(available_from__isnull=True) | Q(available_from__lte=now()),
|
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
|
||||||
Q(available_until__isnull=True) | Q(available_until__gte=now()),
|
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
|
||||||
sales_channels__contains=sales_channel,
|
sales_channels__contains=sales_channel,
|
||||||
active=True,
|
active=True,
|
||||||
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from django.conf import settings
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
Case, Count, F, Func, Max, OuterRef, Q, Subquery, Sum, Value, When,
|
||||||
|
prefetch_related_objects,
|
||||||
)
|
)
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
@@ -446,6 +447,7 @@ class QuotaAvailability:
|
|||||||
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
|
self.results[q] = Quota.AVAILABILITY_RESERVED, 0
|
||||||
|
|
||||||
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
|
def _compute_waitinglist(self, quotas, q_items, q_vars, size_left):
|
||||||
|
prefetch_related_objects(quotas, "event", "event__organizer")
|
||||||
quotas = [
|
quotas = [
|
||||||
q for q in quotas
|
q for q in quotas
|
||||||
if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now()
|
if not q.event.settings.waiting_list_auto_disable or q.event.settings.waiting_list_auto_disable.datetime(q.subevent or q.event) > now()
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ class VATIDTemporaryError(VATIDError):
|
|||||||
|
|
||||||
def _validate_vat_id_NO(vat_id, country_code):
|
def _validate_vat_id_NO(vat_id, country_code):
|
||||||
# Inspired by vat_moss library
|
# Inspired by vat_moss library
|
||||||
vat_id = vat_moss.id.normalize(vat_id)
|
try:
|
||||||
|
vat_id = vat_moss.id.normalize(vat_id)
|
||||||
|
except ValueError:
|
||||||
|
raise VATIDFinalError(error_messages['invalid'])
|
||||||
|
|
||||||
if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]):
|
if not vat_id or len(vat_id) < 3 or not re.match('^\\d{9}MVA$', vat_id[2:]):
|
||||||
raise VATIDFinalError(error_messages['invalid'])
|
raise VATIDFinalError(error_messages['invalid'])
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import sys
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
@@ -49,19 +50,28 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
|
|
||||||
quota_cache = {}
|
quota_cache = {}
|
||||||
gone = set()
|
gone = set()
|
||||||
seats_available = {}
|
_seats_available_cache = {}
|
||||||
|
seats_used = defaultdict(int)
|
||||||
|
|
||||||
for m in SeatCategoryMapping.objects.filter(event=event).select_related('subevent'):
|
seated_product_set = set(
|
||||||
|
SeatCategoryMapping.objects.filter(event=event).values_list('product_id', 'subevent_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
def _seats_available(item, subevent):
|
||||||
# See comment in WaitingListEntry.send_voucher() for rationale
|
# See comment in WaitingListEntry.send_voucher() for rationale
|
||||||
num_free_seets_for_product = (m.subevent or event).free_seats().filter(product_id=m.product_id).count()
|
subevent_id = subevent.pk if subevent else None
|
||||||
num_valid_vouchers_for_product = event.vouchers.filter(
|
if (item.pk, subevent_id) not in _seats_available_cache:
|
||||||
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
num_free_seats_for_product = (subevent or event).free_seats().filter(product_id=item.pk).count()
|
||||||
block_quota=True,
|
num_valid_vouchers_for_product = event.vouchers.filter(
|
||||||
item_id=m.product_id,
|
Q(valid_until__isnull=True) | Q(valid_until__gte=now()),
|
||||||
subevent_id=m.subevent_id,
|
block_quota=True,
|
||||||
waitinglistentries__isnull=False
|
item_id=item.pk,
|
||||||
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
subevent_id=subevent_id,
|
||||||
seats_available[(m.product_id, m.subevent_id)] = num_free_seets_for_product - num_valid_vouchers_for_product
|
waitinglistentries__isnull=False
|
||||||
|
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
|
||||||
|
_seats_available_cache[item.pk, subevent_id] = num_free_seats_for_product - num_valid_vouchers_for_product
|
||||||
|
|
||||||
|
return _seats_available_cache[item.pk, subevent_id] - seats_used[item.pk, subevent_id]
|
||||||
|
|
||||||
prefetch_related_objects(
|
prefetch_related_objects(
|
||||||
[event.organizer],
|
[event.organizer],
|
||||||
@@ -103,7 +113,7 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
|
|
||||||
lock_objects(quotas, shared_lock_objects=[event])
|
lock_objects(quotas, shared_lock_objects=[event])
|
||||||
for wle in qs:
|
for wle in qs:
|
||||||
if (wle.item, wle.variation, wle.subevent) in gone:
|
if (wle.item_id, wle.variation_id, wle.subevent_id) in gone:
|
||||||
continue
|
continue
|
||||||
ev = (wle.subevent or event)
|
ev = (wle.subevent or event)
|
||||||
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
|
if not ev.presale_is_running or (wle.subevent and not wle.subevent.active):
|
||||||
@@ -111,15 +121,15 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
if wle.subevent and not wle.subevent.presale_is_running:
|
if wle.subevent and not wle.subevent.presale_is_running:
|
||||||
continue
|
continue
|
||||||
if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now():
|
if event.settings.waiting_list_auto_disable and event.settings.waiting_list_auto_disable.datetime(wle.subevent or event) <= now():
|
||||||
gone.add((wle.item, wle.variation, wle.subevent))
|
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||||
continue
|
continue
|
||||||
if not wle.item.is_available():
|
if not wle.item.is_available():
|
||||||
gone.add((wle.item, wle.variation, wle.subevent))
|
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
if (wle.item_id, wle.subevent_id) in seated_product_set:
|
||||||
if seats_available[wle.item_id, wle.subevent_id] < 1:
|
if _seats_available(wle.item, wle.subevent) < 1:
|
||||||
gone.add((wle.item, wle.variation, wle.subevent))
|
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
availability = (
|
availability = (
|
||||||
@@ -141,10 +151,10 @@ def assign_automatically(event: Event, user_id: int=None, subevent_id: int=None)
|
|||||||
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
|
quota_cache[q.pk][1] - 1 if quota_cache[q.pk][1] is not None else sys.maxsize
|
||||||
)
|
)
|
||||||
|
|
||||||
if (wle.item_id, wle.subevent_id) in seats_available:
|
if (wle.item_id, wle.subevent_id) in seated_product_set:
|
||||||
seats_available[wle.item_id, wle.subevent_id] -= 1
|
seats_used[wle.item_id, wle.subevent_id] += 1
|
||||||
else:
|
else:
|
||||||
gone.add((wle.item, wle.variation, wle.subevent))
|
gone.add((wle.item_id, wle.variation_id, wle.subevent_id))
|
||||||
|
|
||||||
return sent
|
return sent
|
||||||
|
|
||||||
|
|||||||
+26
-21
@@ -971,7 +971,8 @@ DEFAULTS = {
|
|||||||
},
|
},
|
||||||
'payment_giftcard__enabled': {
|
'payment_giftcard__enabled': {
|
||||||
'default': 'True',
|
'default': 'True',
|
||||||
'type': bool
|
'type': bool,
|
||||||
|
'serializer_class': serializers.BooleanField,
|
||||||
},
|
},
|
||||||
'payment_giftcard_public_name': {
|
'payment_giftcard_public_name': {
|
||||||
'default': LazyI18nString.from_gettext(gettext_noop('Gift card')),
|
'default': LazyI18nString.from_gettext(gettext_noop('Gift card')),
|
||||||
@@ -1653,6 +1654,28 @@ DEFAULTS = {
|
|||||||
"calendar.")
|
"calendar.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'allow_modifications': {
|
||||||
|
'default': 'order',
|
||||||
|
'type': str,
|
||||||
|
'form_class': forms.ChoiceField,
|
||||||
|
'serializer_class': serializers.ChoiceField,
|
||||||
|
'serializer_kwargs': dict(
|
||||||
|
choices=(
|
||||||
|
('no', _('No modifications after order was submitted')),
|
||||||
|
('order', _('Only the person who ordered can make changes')),
|
||||||
|
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_("Allow customers to modify their information"),
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
choices=(
|
||||||
|
('no', _('No modifications after order was submitted')),
|
||||||
|
('order', _('Only the person who ordered can make changes')),
|
||||||
|
('attendee', _('Both the attendee and the person who ordered can make changes')),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
'allow_modifications_after_checkin': {
|
'allow_modifications_after_checkin': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -1660,6 +1683,8 @@ DEFAULTS = {
|
|||||||
'serializer_class': serializers.BooleanField,
|
'serializer_class': serializers.BooleanField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_("Allow customers to modify their information after they checked in."),
|
label=_("Allow customers to modify their information after they checked in."),
|
||||||
|
help_text=_("By default, no more modifications are possible for an order as soon as one of the tickets "
|
||||||
|
"in the order has been checked in.")
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
'last_order_modification_date': {
|
'last_order_modification_date': {
|
||||||
@@ -2826,22 +2851,6 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
**primary_font_kwargs()
|
**primary_font_kwargs()
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
'presale_css_file': {
|
|
||||||
'default': None,
|
|
||||||
'type': str
|
|
||||||
},
|
|
||||||
'presale_css_checksum': {
|
|
||||||
'default': None,
|
|
||||||
'type': str
|
|
||||||
},
|
|
||||||
'presale_widget_css_file': {
|
|
||||||
'default': None,
|
|
||||||
'type': str
|
|
||||||
},
|
|
||||||
'presale_widget_css_checksum': {
|
|
||||||
'default': None,
|
|
||||||
'type': str
|
|
||||||
},
|
|
||||||
'logo_image': {
|
'logo_image': {
|
||||||
'default': None,
|
'default': None,
|
||||||
'type': File,
|
'type': File,
|
||||||
@@ -3371,10 +3380,6 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'type': str,
|
'type': str,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SETTINGS_AFFECTING_CSS = {
|
|
||||||
'primary_color', 'theme_color_success', 'theme_color_danger', 'primary_font',
|
|
||||||
'theme_color_background', 'theme_round_borders'
|
|
||||||
}
|
|
||||||
PERSON_NAME_TITLE_GROUPS = OrderedDict([
|
PERSON_NAME_TITLE_GROUPS = OrderedDict([
|
||||||
('english_common', (_('Most common English titles'), (
|
('english_common', (_('Most common English titles'), (
|
||||||
'Mr',
|
'Mr',
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#
|
||||||
|
# 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 contextvars
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from dateutil.parser import parse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
timemachine_now_var = contextvars.ContextVar('timemachine_now', default=None)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def time_machine_now_assigned_from_request(request):
|
||||||
|
if hasattr(request, 'event') and f'timemachine_now_dt:{request.event.pk}' in request.session and \
|
||||||
|
request.event.testmode and has_time_machine_permission(request, request.event):
|
||||||
|
request.now_dt = parse(request.session[f'timemachine_now_dt:{request.event.pk}'])
|
||||||
|
request.now_dt_is_fake = True
|
||||||
|
else:
|
||||||
|
request.now_dt = now()
|
||||||
|
request.now_dt_is_fake = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
timemachine_now_var.set(request.now_dt if request.now_dt_is_fake else None)
|
||||||
|
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
timemachine_now_var.set(None)
|
||||||
|
|
||||||
|
|
||||||
|
def time_machine_now(default=False):
|
||||||
|
"""
|
||||||
|
Return the datetime to use as current datetime for checking order restrictions in event
|
||||||
|
index and checkout flow.
|
||||||
|
|
||||||
|
:param default: Value to return if time machine mode is disabled. By default the current datetime is used.
|
||||||
|
"""
|
||||||
|
if default is False:
|
||||||
|
default = now()
|
||||||
|
return timemachine_now_var.get() or default
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def time_machine_now_assigned(now_dt):
|
||||||
|
"""
|
||||||
|
Use this context manager to assign current datetime for time machine mode. Useful e.g. for background tasks.
|
||||||
|
|
||||||
|
:param now_dt: The datetime value to assign. May be `None` to disable time machine.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
timemachine_now_var.set(now_dt)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
timemachine_now_var.set(None)
|
||||||
|
|
||||||
|
|
||||||
|
def has_time_machine_permission(request, event):
|
||||||
|
permission = 'can_change_event_settings'
|
||||||
|
|
||||||
|
return (
|
||||||
|
request.user.is_authenticated and
|
||||||
|
request.user.has_event_permission(request.organizer, request.event, permission, request=request)
|
||||||
|
) or (
|
||||||
|
getattr(request, 'event_access_user', None) and
|
||||||
|
request.event_access_user.is_authenticated and
|
||||||
|
request.event_access_user.has_event_permission(request.organizer, request.event, permission,
|
||||||
|
session_key=request.event_access_parent_session_key)
|
||||||
|
)
|
||||||
@@ -46,11 +46,11 @@ from pretix.base.settings import GlobalSettingsObject
|
|||||||
from pretix.control.navigation import (
|
from pretix.control.navigation import (
|
||||||
get_event_navigation, get_global_navigation, get_organizer_navigation,
|
get_event_navigation, get_global_navigation, get_organizer_navigation,
|
||||||
)
|
)
|
||||||
|
from pretix.helpers.i18n import (
|
||||||
from ..helpers.i18n import (
|
|
||||||
get_javascript_format, get_javascript_output_format, get_moment_locale,
|
get_javascript_format, get_javascript_output_format, get_moment_locale,
|
||||||
)
|
)
|
||||||
from ..multidomain.urlreverse import get_event_domain
|
from pretix.multidomain.urlreverse import get_event_domain
|
||||||
|
|
||||||
from .signals import html_head, nav_topbar
|
from .signals import html_head, nav_topbar
|
||||||
|
|
||||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
@@ -106,7 +106,7 @@ def _default_context(request):
|
|||||||
else:
|
else:
|
||||||
ctx['complain_testmode_orders'] = False
|
ctx['complain_testmode_orders'] = False
|
||||||
|
|
||||||
if not request.event.live and ctx['has_domain']:
|
if (request.event.testmode or not request.event.live) and ctx['has_domain']:
|
||||||
child_sess = request.session.get('child_session_{}'.format(request.event.pk))
|
child_sess = request.session.get('child_session_{}'.format(request.event.pk))
|
||||||
s = SessionStore()
|
s = SessionStore()
|
||||||
if not child_sess or not s.exists(child_sess):
|
if not child_sess or not s.exists(child_sess):
|
||||||
@@ -114,10 +114,8 @@ def _default_context(request):
|
|||||||
s.create()
|
s.create()
|
||||||
ctx['new_session'] = s.session_key
|
ctx['new_session'] = s.session_key
|
||||||
request.session['child_session_{}'.format(request.event.pk)] = s.session_key
|
request.session['child_session_{}'.format(request.event.pk)] = s.session_key
|
||||||
request.session['event_access'] = True
|
|
||||||
else:
|
else:
|
||||||
ctx['new_session'] = child_sess
|
ctx['new_session'] = child_sess
|
||||||
request.session['event_access'] = True
|
|
||||||
|
|
||||||
if request.GET.get('subevent', ''):
|
if request.GET.get('subevent', ''):
|
||||||
# Do not use .get() for lazy evaluation
|
# Do not use .get() for lazy evaluation
|
||||||
|
|||||||
@@ -580,6 +580,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
|||||||
'banner_text',
|
'banner_text',
|
||||||
'banner_text_bottom',
|
'banner_text_bottom',
|
||||||
'order_email_asked_twice',
|
'order_email_asked_twice',
|
||||||
|
'allow_modifications',
|
||||||
'last_order_modification_date',
|
'last_order_modification_date',
|
||||||
'allow_modifications_after_checkin',
|
'allow_modifications_after_checkin',
|
||||||
'checkout_show_copy_answers_button',
|
'checkout_show_copy_answers_button',
|
||||||
|
|||||||
@@ -698,6 +698,14 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
'tax_rule',
|
'tax_rule',
|
||||||
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
|
_("Gift card products should use a tax rule with a rate of 0 percent since sales tax will be applied when the gift card is redeemed.")
|
||||||
)
|
)
|
||||||
|
if d.get('validity_mode'):
|
||||||
|
self.add_error(
|
||||||
|
'validity_mode',
|
||||||
|
_(
|
||||||
|
"Do not set a specific validity for gift card products as it will not restrict the validity "
|
||||||
|
"of the gift card. A validity of gift cards can be set in your organizer settings."
|
||||||
|
)
|
||||||
|
)
|
||||||
if d.get('admission'):
|
if d.get('admission'):
|
||||||
self.add_error(
|
self.add_error(
|
||||||
'admission',
|
'admission',
|
||||||
@@ -736,6 +744,14 @@ class ItemUpdateForm(I18nModelForm):
|
|||||||
_("The start of validity must be before the end of validity.")
|
_("The start of validity must be before the end of validity.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if d.get('validity_mode') == Item.VALIDITY_MODE_DYNAMIC:
|
||||||
|
if not any(d.get(f'validity_dynamic_duration_{k}') for k in ('months', 'days', 'hours', 'minutes')):
|
||||||
|
self.add_error(
|
||||||
|
'validity_dynamic_duration_months',
|
||||||
|
_("You have selected dynamic validity but have not entered a time period. This would render "
|
||||||
|
"the tickets unusable.")
|
||||||
|
)
|
||||||
|
|
||||||
Item.clean_media_settings(self.event, d.get('media_policy'), d.get('media_type'), d.get('issue_giftcard'))
|
Item.clean_media_settings(self.event, d.get('media_policy'), d.get('media_type'), d.get('issue_giftcard'))
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ class VoucherForm(I18nModelForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
instance = kwargs.get('instance')
|
instance = kwargs.get('instance')
|
||||||
initial = kwargs.get('initial')
|
initial = kwargs.get('initial')
|
||||||
|
self.initial_instance_data = None
|
||||||
if instance:
|
if instance:
|
||||||
self.initial_instance_data = modelcopy(instance)
|
if instance.pk:
|
||||||
|
self.initial_instance_data = modelcopy(instance)
|
||||||
try:
|
try:
|
||||||
if instance.variation:
|
if instance.variation:
|
||||||
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
initial['itemvar'] = '%d-%d' % (instance.item.pk, instance.variation.pk)
|
||||||
@@ -101,8 +103,6 @@ class VoucherForm(I18nModelForm):
|
|||||||
initial['itemvar'] = 'q-%d' % instance.quota.pk
|
initial['itemvar'] = 'q-%d' % instance.quota.pk
|
||||||
except Item.DoesNotExist:
|
except Item.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
self.initial_instance_data = None
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if instance.event.has_subevents:
|
if instance.event.has_subevents:
|
||||||
@@ -234,8 +234,8 @@ class VoucherForm(I18nModelForm):
|
|||||||
)
|
)
|
||||||
if check_quota:
|
if check_quota:
|
||||||
Voucher.clean_quota_check(
|
Voucher.clean_quota_check(
|
||||||
data, cnt, self.initial_instance_data, self.instance.event,
|
data, cnt, self.initial_instance_data,
|
||||||
self.instance.quota, self.instance.item, self.instance.variation
|
self.instance.event, self.instance.quota, self.instance.item, self.instance.variation
|
||||||
)
|
)
|
||||||
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
Voucher.clean_voucher_code(data, self.instance.event, self.instance.pk)
|
||||||
if 'seat' in self.fields and data.get('seat'):
|
if 'seat' in self.fields and data.get('seat'):
|
||||||
@@ -366,14 +366,14 @@ class VoucherBulkForm(VoucherForm):
|
|||||||
raise ValidationError(_('CSV input contains an unknown field with the header "{header}".').format(header=unknown_fields[0]))
|
raise ValidationError(_('CSV input contains an unknown field with the header "{header}".').format(header=unknown_fields[0]))
|
||||||
for i, row in enumerate(reader):
|
for i, row in enumerate(reader):
|
||||||
try:
|
try:
|
||||||
EmailValidator()(row['email'])
|
EmailValidator()(row['email'].strip())
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
raise ValidationError(_('{value} is not a valid email address.').format(value=row['email'])) from err
|
raise ValidationError(_('{value} is not a valid email address.').format(value=row['email'].strip())) from err
|
||||||
try:
|
try:
|
||||||
res.append(self.Recipient(
|
res.append(self.Recipient(
|
||||||
name=row.get('name', ''),
|
name=row.get('name', ''),
|
||||||
email=row['email'].strip(),
|
email=row['email'].strip(),
|
||||||
number=int(row.get('number', 1)),
|
number=int(row.get('number', 1) or ""),
|
||||||
tag=row.get('tag', None)
|
tag=row.get('tag', None)
|
||||||
))
|
))
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
|
|||||||
@@ -444,6 +444,11 @@ def get_global_navigation(request):
|
|||||||
'url': reverse('control:global.license'),
|
'url': reverse('control:global.license'),
|
||||||
'active': (url.url_name == 'global.license'),
|
'active': (url.url_name == 'global.license'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'label': _('System report'),
|
||||||
|
'url': reverse('control:global.sysreport'),
|
||||||
|
'active': (url.url_name == 'global.sysreport'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,307 @@
|
|||||||
|
#
|
||||||
|
# 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 os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import zoneinfo
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import Count, Exists, F, Min, OuterRef, Q, Sum
|
||||||
|
from django.utils.formats import date_format
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from reportlab.lib import pagesizes
|
||||||
|
from reportlab.lib.enums import TA_CENTER
|
||||||
|
from reportlab.lib.units import mm
|
||||||
|
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
|
||||||
|
|
||||||
|
from pretix import __version__
|
||||||
|
from pretix.base.models import Order, OrderPayment, Transaction
|
||||||
|
from pretix.base.plugins import get_all_plugins
|
||||||
|
from pretix.base.templatetags.money import money_filter
|
||||||
|
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||||
|
from pretix.settings import DATA_DIR
|
||||||
|
|
||||||
|
|
||||||
|
class SysReport(ReportlabExportMixin):
|
||||||
|
@property
|
||||||
|
def pagesize(self):
|
||||||
|
return pagesizes.portrait(pagesizes.A4)
|
||||||
|
|
||||||
|
def __init__(self, start_month, tzname):
|
||||||
|
self.tzname = tzname
|
||||||
|
self.tz = zoneinfo.ZoneInfo(tzname)
|
||||||
|
self.start_month = start_month
|
||||||
|
|
||||||
|
def page_header(self, canvas, doc):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def page_footer(self, canvas, doc):
|
||||||
|
from reportlab.lib.units import mm
|
||||||
|
|
||||||
|
canvas.setFont("OpenSans", 8)
|
||||||
|
canvas.drawString(15 * mm, 10 * mm, "Page %d" % doc.page)
|
||||||
|
canvas.drawRightString(
|
||||||
|
self.pagesize[0] - doc.rightMargin,
|
||||||
|
10 * mm,
|
||||||
|
"Created: %s"
|
||||||
|
% date_format(now().astimezone(self.tz), "SHORT_DATETIME_FORMAT"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
return "sysreport.pdf", "application/pdf", self.create({})
|
||||||
|
|
||||||
|
def get_story(self, doc, form_data):
|
||||||
|
headlinestyle = self.get_style()
|
||||||
|
headlinestyle.fontSize = 15
|
||||||
|
subheadlinestyle = self.get_style()
|
||||||
|
subheadlinestyle.fontSize = 13
|
||||||
|
style_small = self.get_style()
|
||||||
|
style_small.fontSize = 6
|
||||||
|
|
||||||
|
story = [
|
||||||
|
Paragraph("System report", headlinestyle),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
Paragraph("Usage", subheadlinestyle),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
self._usage_table(),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
Paragraph("Installed versions", subheadlinestyle),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
self._tech_table(),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
Paragraph("Plugins", subheadlinestyle),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
Paragraph(self._get_plugin_versions(), style_small),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
Paragraph("Custom templates", subheadlinestyle),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
Paragraph(self._get_custom_templates(), style_small),
|
||||||
|
Spacer(1, 5 * mm),
|
||||||
|
]
|
||||||
|
|
||||||
|
return story
|
||||||
|
|
||||||
|
def _tech_table(self):
|
||||||
|
style = self.get_style()
|
||||||
|
style.fontSize = 8
|
||||||
|
style_small = self.get_style()
|
||||||
|
style_small.fontSize = 6
|
||||||
|
|
||||||
|
w = self.pagesize[0] - 30 * mm
|
||||||
|
colwidths = [
|
||||||
|
a * w
|
||||||
|
for a in (
|
||||||
|
0.2,
|
||||||
|
0.8,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
tstyledata = [
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
("LEFTPADDING", (0, 0), (0, -1), 0),
|
||||||
|
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
||||||
|
]
|
||||||
|
tdata = [
|
||||||
|
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
||||||
|
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
|
||||||
|
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
|
||||||
|
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
|
||||||
|
[
|
||||||
|
Paragraph("Database engine:", style),
|
||||||
|
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
||||||
|
table.setStyle(TableStyle(tstyledata))
|
||||||
|
return table
|
||||||
|
|
||||||
|
def _usage_table(self):
|
||||||
|
style = self.get_style()
|
||||||
|
style.fontSize = 8
|
||||||
|
style_small = self.get_style()
|
||||||
|
style_small.fontSize = 6
|
||||||
|
style_small.leading = 8
|
||||||
|
style_small.alignment = TA_CENTER
|
||||||
|
style_small_head = self.get_style()
|
||||||
|
style_small_head.fontSize = 6
|
||||||
|
style_small_head.leading = 8
|
||||||
|
style_small_head.alignment = TA_CENTER
|
||||||
|
style_small_head.fontName = "OpenSansBd"
|
||||||
|
|
||||||
|
w = self.pagesize[0] - 30 * mm
|
||||||
|
|
||||||
|
successful = (
|
||||||
|
Q(status=Order.STATUS_PAID)
|
||||||
|
| Q(valid_if_pending=True, status=Order.STATUS_PENDING)
|
||||||
|
| Q(
|
||||||
|
Exists(
|
||||||
|
OrderPayment.objects.filter(
|
||||||
|
order_id=OuterRef("pk"),
|
||||||
|
state__in=(
|
||||||
|
OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||||
|
OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
orders_q = Order.objects.filter(
|
||||||
|
successful,
|
||||||
|
testmode=False,
|
||||||
|
)
|
||||||
|
orders_testmode_q = Order.objects.filter(
|
||||||
|
testmode=True,
|
||||||
|
)
|
||||||
|
orders_unconfirmed_q = Order.objects.filter(
|
||||||
|
~successful,
|
||||||
|
testmode=False,
|
||||||
|
)
|
||||||
|
revenue_q = Transaction.objects.filter(
|
||||||
|
Exists(
|
||||||
|
OrderPayment.objects.filter(
|
||||||
|
order_id=OuterRef("order_id"),
|
||||||
|
state__in=(
|
||||||
|
OrderPayment.PAYMENT_STATE_CONFIRMED,
|
||||||
|
OrderPayment.PAYMENT_STATE_REFUNDED,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
order__testmode=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
currencies = sorted(
|
||||||
|
list(
|
||||||
|
set(
|
||||||
|
Transaction.objects.annotate(c=F("order__event__currency"))
|
||||||
|
.values_list("c", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
year_first = orders_q.aggregate(m=Min("datetime__year"))["m"]
|
||||||
|
if not year_first:
|
||||||
|
year_first = now().year
|
||||||
|
elif datetime.now().month - 1 <= self.start_month:
|
||||||
|
year_first -= 1
|
||||||
|
year_last = now().year
|
||||||
|
tdata = [
|
||||||
|
[
|
||||||
|
Paragraph(l, style_small_head)
|
||||||
|
for l in (
|
||||||
|
"Time frame",
|
||||||
|
"Currency",
|
||||||
|
"Successful orders",
|
||||||
|
"Net revenue",
|
||||||
|
"Testmode orders",
|
||||||
|
"Unsucessful orders",
|
||||||
|
"Positions",
|
||||||
|
"Gross revenue",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
for year in range(year_first, year_last + 1):
|
||||||
|
for i, c in enumerate(currencies):
|
||||||
|
first_day = datetime(
|
||||||
|
year, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz
|
||||||
|
)
|
||||||
|
after_day = datetime(
|
||||||
|
year + 1, self.start_month, 1, 0, 0, 0, 0, tzinfo=self.tz
|
||||||
|
)
|
||||||
|
|
||||||
|
orders_count = (
|
||||||
|
orders_q.filter(
|
||||||
|
datetime__gte=first_day, datetime__lt=after_day
|
||||||
|
).aggregate(c=Count("*"))["c"]
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
testmode_count = (
|
||||||
|
orders_testmode_q.filter(
|
||||||
|
datetime__gte=first_day, datetime__lt=after_day
|
||||||
|
).aggregate(c=Count("*"))["c"]
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
unconfirmed_count = (
|
||||||
|
orders_unconfirmed_q.filter(
|
||||||
|
datetime__gte=first_day, datetime__lt=after_day
|
||||||
|
).aggregate(c=Count("*"))["c"]
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
revenue_data = revenue_q.filter(
|
||||||
|
datetime__gte=first_day, datetime__lt=after_day, order__event__currency=c
|
||||||
|
).aggregate(
|
||||||
|
c=Sum("count"),
|
||||||
|
s_net=Sum(F("price") - F("tax_value")),
|
||||||
|
s_gross=Sum(F("price")),
|
||||||
|
)
|
||||||
|
|
||||||
|
tdata.append(
|
||||||
|
(
|
||||||
|
Paragraph(
|
||||||
|
date_format(first_day, "M Y")
|
||||||
|
+ " – "
|
||||||
|
+ date_format(after_day - timedelta(days=1), "M Y"),
|
||||||
|
style_small,
|
||||||
|
),
|
||||||
|
Paragraph(c, style_small),
|
||||||
|
Paragraph(str(orders_count), style_small) if i == 0 else "",
|
||||||
|
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
||||||
|
Paragraph(str(testmode_count), style_small) if i == 0 else "",
|
||||||
|
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
||||||
|
Paragraph(str(revenue_data.get("c") or 0), style_small),
|
||||||
|
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
colwidths = [a * w for a in (0.18,) + (0.82 / 7,) * 7]
|
||||||
|
tstyledata = [
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
("LEFTPADDING", (0, 0), (0, -1), 0),
|
||||||
|
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
||||||
|
("TOPPADDING", (0, 0), (-1, -1), 0),
|
||||||
|
("BOTTOMPADDING", (0, 0), (-1, -1), 1),
|
||||||
|
]
|
||||||
|
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
||||||
|
table.setStyle(TableStyle(tstyledata))
|
||||||
|
return table
|
||||||
|
|
||||||
|
def _get_plugin_versions(self):
|
||||||
|
lines = []
|
||||||
|
for p in get_all_plugins():
|
||||||
|
lines.append(f"{p.name} {p.version}")
|
||||||
|
return ", ".join(lines)
|
||||||
|
|
||||||
|
def _get_custom_templates(self):
|
||||||
|
lines = []
|
||||||
|
for dirpath, dirnames, filenames in os.walk(
|
||||||
|
os.path.join(DATA_DIR, "templates")
|
||||||
|
):
|
||||||
|
for f in filenames:
|
||||||
|
lines.append(f"{dirpath}/{f}")
|
||||||
|
|
||||||
|
d = "<br/>".join(lines[:50])
|
||||||
|
if len(lines) > 50:
|
||||||
|
d += "<br/>..."
|
||||||
|
if not d:
|
||||||
|
return "–"
|
||||||
|
return d
|
||||||
@@ -327,8 +327,7 @@
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
Internet Explorer is an old browser that does not support lots of recent web-based
|
Internet Explorer is an old browser that does not support lots of recent web-based
|
||||||
technologies. While some features might already not work properly, we plan on no longer
|
technologies and is no longer supported by this website.
|
||||||
supporting Internet Explorer in our administrative backend in the next months.
|
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
We kindly ask you to move to one of our supported browsers, such as Microsoft Edge,
|
We kindly ask you to move to one of our supported browsers, such as Microsoft Edge,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="test-right">
|
<div class="text-right">
|
||||||
<button type="submit" class="btn btn-primary btn-lg btn-save" disabled>
|
<button type="submit" class="btn btn-primary btn-lg btn-save" disabled>
|
||||||
{% trans "Go live" %}
|
{% trans "Go live" %}
|
||||||
</button>
|
</button>
|
||||||
@@ -82,10 +82,10 @@
|
|||||||
<p>
|
<p>
|
||||||
{% trans "Your shop is currently in test mode. All orders are not persistent and can be deleted at any point." %}
|
{% trans "Your shop is currently in test mode. All orders are not persistent and can be deleted at any point." %}
|
||||||
</p>
|
</p>
|
||||||
<div class="form-inline">
|
<div class="checkbox">
|
||||||
<label class="checkbox">
|
<label>
|
||||||
<input type="checkbox" name="delete" value="yes" />
|
<input type="checkbox" name="delete" value="yes" />
|
||||||
{% trans "Permanently delete all orders created in test mode" %}
|
<b>{% trans "Permanently delete all orders created in test mode" %}</b>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
|
|||||||
@@ -113,10 +113,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
|
{% bootstrap_field sform.attendee_data_explanation_text layout="control" %}
|
||||||
|
|
||||||
<h4>{% trans "Other settings" %}</h4>
|
<h4>{% trans "Form settings" %}</h4>
|
||||||
{% bootstrap_field sform.name_scheme layout="control" %}
|
{% bootstrap_field sform.name_scheme layout="control" %}
|
||||||
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
{% bootstrap_field sform.name_scheme_titles layout="control" %}
|
||||||
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
|
{% bootstrap_field sform.checkout_show_copy_answers_button layout="control" %}
|
||||||
|
|
||||||
|
<h4>{% trans "Changes to existing orders" %}</h4>
|
||||||
|
{% bootstrap_field sform.allow_modifications layout="control" %}
|
||||||
|
<div data-display-dependency='#id_settings-allow_modifications_0' data-inverse>
|
||||||
|
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
||||||
|
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Texts" %}</legend>
|
<legend>{% trans "Texts" %}</legend>
|
||||||
@@ -225,21 +232,47 @@
|
|||||||
{% bootstrap_field sform.presale_start_show_date layout="control" %}
|
{% bootstrap_field sform.presale_start_show_date layout="control" %}
|
||||||
{% bootstrap_field form.presale_end layout="control" %}
|
{% bootstrap_field form.presale_end layout="control" %}
|
||||||
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
{% bootstrap_field sform.show_items_outside_presale_period layout="control" %}
|
||||||
{% bootstrap_field sform.last_order_modification_date layout="control" %}
|
|
||||||
{% bootstrap_field sform.allow_modifications_after_checkin layout="control" %}
|
|
||||||
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Display" %}</legend>
|
<legend>{% trans "Display" %}</legend>
|
||||||
|
<h4>{% trans "Date and time" %}</h4>
|
||||||
{% bootstrap_field sform.show_dates_on_frontpage layout="control" %}
|
{% bootstrap_field sform.show_dates_on_frontpage layout="control" %}
|
||||||
{% bootstrap_field sform.show_date_to layout="control" %}
|
{% bootstrap_field sform.show_date_to layout="control" %}
|
||||||
{% bootstrap_field sform.show_times layout="control" %}
|
{% bootstrap_field sform.show_times layout="control" %}
|
||||||
|
<h4>{% trans "Product list" %}</h4>
|
||||||
{% bootstrap_field sform.show_quota_left layout="control" %}
|
{% bootstrap_field sform.show_quota_left layout="control" %}
|
||||||
{% bootstrap_field sform.display_net_prices layout="control" %}
|
{% bootstrap_field sform.display_net_prices layout="control" %}
|
||||||
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
|
|
||||||
{% bootstrap_field sform.show_variations_expanded layout="control" %}
|
{% bootstrap_field sform.show_variations_expanded layout="control" %}
|
||||||
{% bootstrap_field sform.hide_sold_out layout="control" %}
|
{% bootstrap_field sform.hide_sold_out layout="control" %}
|
||||||
|
|
||||||
|
<h4>{% trans "Calendar and list views" context "subevents" %}</h4>
|
||||||
|
{% if sform.frontpage_subevent_ordering %}
|
||||||
|
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_list_type %}
|
||||||
|
{% bootstrap_field sform.event_list_type layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_list_available_only %}
|
||||||
|
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_list_filters %}
|
||||||
|
{% bootstrap_field sform.event_list_filters layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if sform.event_calendar_future_only %}
|
||||||
|
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
||||||
|
|
||||||
|
|
||||||
|
<h4>{% trans "Order details" %}</h4>
|
||||||
|
{% bootstrap_field sform.hide_prices_from_attendees layout="control" %}
|
||||||
|
{% bootstrap_field sform.show_checkin_number_user layout="control" %}
|
||||||
|
|
||||||
|
<h4>{% trans "Other settings" %}</h4>
|
||||||
|
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||||
|
{% propagated request.event org_url "meta_noindex" %}
|
||||||
|
{% bootstrap_field sform.meta_noindex layout="control" %}
|
||||||
|
{% endpropagated %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label">
|
<label class="col-md-3 control-label">
|
||||||
{% trans "Footer links" %}<br>
|
{% trans "Footer links" %}<br>
|
||||||
@@ -306,28 +339,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if sform.frontpage_subevent_ordering %}
|
|
||||||
{% bootstrap_field sform.frontpage_subevent_ordering layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_list_type %}
|
|
||||||
{% bootstrap_field sform.event_list_type layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_list_available_only %}
|
|
||||||
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_list_filters %}
|
|
||||||
{% bootstrap_field sform.event_list_filters layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if sform.event_calendar_future_only %}
|
|
||||||
{% bootstrap_field sform.event_calendar_future_only layout="control" %}
|
|
||||||
{% endif %}
|
|
||||||
{% bootstrap_field sform.low_availability_percentage layout="control" addon_after="%" %}
|
|
||||||
|
|
||||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
|
||||||
{% propagated request.event org_url "meta_noindex" %}
|
|
||||||
{% bootstrap_field sform.meta_noindex layout="control" %}
|
|
||||||
{% endpropagated %}
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{% trans "Cart" %}</legend>
|
<legend>{% trans "Cart" %}</legend>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{% load eventurl %}
|
||||||
|
{% load static %}
|
||||||
|
<form action="{% eventurl request.event "presale:event.auth" %}{% if request.GET.next %}?next={{ request.GET.next }}{% endif %}" method="post">
|
||||||
|
<input type="hidden" value="{{ new_session }}" name="session">
|
||||||
|
<button type="submit">
|
||||||
|
Continue
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<script src="{% static "pretixcontrol/js/send_form.js" %}"></script>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{% extends "pretixcontrol/global_settings_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block inner %}
|
||||||
|
<p>
|
||||||
|
{% trans "If you have a pretix Enterprise license, this report must be submitted to pretix support when your license renews. It may also be requested by pretix support to aid debugging of problems." %}
|
||||||
|
{% trans "It serves two purposes: Collecting useful information that might help with debugging problems in your pretix installation, and verifying that your usage of pretix is in compliance with the Enterprise license you purchased." %}
|
||||||
|
</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>
|
||||||
|
<label>
|
||||||
|
{% trans "First month of license term:" %}
|
||||||
|
<select name="month" class="form-control">
|
||||||
|
<option value="1">{% trans "January" %}</option>
|
||||||
|
<option value="2">{% trans "February" %}</option>
|
||||||
|
<option value="3">{% trans "March" %}</option>
|
||||||
|
<option value="4">{% trans "April" %}</option>
|
||||||
|
<option value="5">{% trans "May" %}</option>
|
||||||
|
<option value="6">{% trans "June" %}</option>
|
||||||
|
<option value="7">{% trans "July" %}</option>
|
||||||
|
<option value="8">{% trans "August" %}</option>
|
||||||
|
<option value="9">{% trans "September" %}</option>
|
||||||
|
<option value="10">{% trans "October" %}</option>
|
||||||
|
<option value="11">{% trans "November" %}</option>
|
||||||
|
<option value="11">{% trans "December" %}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</p>
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">
|
||||||
|
{% trans "Generate report" %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Product categories" %}</th>
|
<th>{% trans "Product categories" %}</th>
|
||||||
<th class="action-col-2"></th>
|
<th class="action-col-2"></th>
|
||||||
<th class="action-col-2"></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
|
<tbody data-dnd-url="{% url "control:event.items.categories.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||||
@@ -41,18 +40,16 @@
|
|||||||
<td>
|
<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>
|
<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>
|
||||||
<td>
|
|
||||||
<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">
|
<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>
|
<button title="{% trans "Move up" %}" 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 title="{% trans "Move down" %}" 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" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||||
|
<a title="{% trans "Edit" %}" 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>
|
||||||
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
|
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
|
||||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||||
<span class="fa fa-copy"></span>
|
<span class="fa fa-copy"></span>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -56,8 +56,7 @@
|
|||||||
<th>{% trans "Internal name" %}</th>
|
<th>{% trans "Internal name" %}</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>{% trans "Products" %}</th>
|
<th colspan="2">{% trans "Products" %}</th>
|
||||||
<th class="action-col-2"></th>
|
|
||||||
<th class="action-col-2"></th>
|
<th class="action-col-2"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -102,9 +101,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td {% if d.benefit_same_products %}colspan="2"{% endif %}>
|
||||||
|
{% if not d.benefit_same_products %}{% trans "Condition:" %}{% endif %}
|
||||||
{% if d.condition_all_products %}
|
{% if d.condition_all_products %}
|
||||||
<em>{% trans "All" %}</em>
|
<ul><li><em>{% trans "All" %}</em></li></ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for item in d.condition_limit_products.all %}
|
{% for item in d.condition_limit_products.all %}
|
||||||
@@ -115,18 +115,28 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
{% if not d.benefit_same_products %}
|
||||||
|
<td>
|
||||||
|
{% trans "Applies to:" %}
|
||||||
|
<ul>
|
||||||
|
{% for item in d.benefit_limit_products.all %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td class="text-right flip">
|
||||||
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||||
class="btn btn-default btn-sm sortable-up"
|
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
|
||||||
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
|
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
|
||||||
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
|
||||||
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||||
class="btn btn-default btn-sm sortable-down"
|
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
|
||||||
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
|
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
|
||||||
<i class="fa fa-arrow-down"></i></button>
|
<i class="fa fa-arrow-down"></i></button>
|
||||||
<span class="dnd-container"></span>
|
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||||
</td>
|
|
||||||
<td class="text-right flip">
|
|
||||||
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
|
||||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
|
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
{% extends "pretixcontrol/items/base.html" %}
|
{% extends "pretixcontrol/items/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load money %}
|
||||||
{% block title %}{% trans "Products" %}{% endblock %}
|
{% block title %}{% trans "Products" %}{% endblock %}
|
||||||
{% block inside %}
|
{% block inside %}
|
||||||
|
{% blocktrans asvar s_taxes %}taxes{% endblocktrans %}
|
||||||
<h1>{% trans "Products" %}</h1>
|
<h1>{% trans "Products" %}</h1>
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
Below, you find a list of all available products. You can click on a product name to inspect and change
|
Below, you find a list of all available products. You can click on a product name to inspect and change
|
||||||
product details. You can also use the buttons on the right to change the order of products within a
|
product details. You can also use the buttons on the right to change the order of products or move
|
||||||
give category.
|
products to a different category.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
{% if items|length == 0 %}
|
{% if items|length == 0 %}
|
||||||
@@ -29,7 +31,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-condensed table-hover">
|
<table class="table table-condensed table-hover table-items">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Product name" %}</th>
|
<th>{% trans "Product name" %}</th>
|
||||||
@@ -37,16 +39,24 @@
|
|||||||
<th class="iconcol"></th>
|
<th class="iconcol"></th>
|
||||||
<th class="iconcol"></th>
|
<th class="iconcol"></th>
|
||||||
<th class="iconcol"></th>
|
<th class="iconcol"></th>
|
||||||
<th>{% trans "Category" %}</th>
|
<th class="text-right flip">{% trans "Default price" %}</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>
|
<th class="action-col-2"><span class="sr-only">Edit</span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% regroup items by category as cat_list %}
|
|
||||||
{% for c in cat_list %}
|
{% for c, items in cat_list %}
|
||||||
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug %}">
|
{% if c %}
|
||||||
{% for i in c.list %}
|
<tbody>
|
||||||
{% if forloop.counter0 == 0 and i.category %}<tr class="sortable-disabled"><th colspan="8" scope="colgroup" class="text-muted">{{ i.category }}</th></tr>{% endif %}
|
<tr class="sortable-disabled"><th colspan="9" scope="colgroup" class="text-muted">
|
||||||
|
{{ c.internal_name|default:c.name }}{% if c.category_type != "normal" %} <span class="font-normal">({{ c.get_category_type_display }})</span>{% endif %}
|
||||||
|
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
|
||||||
|
</th></tr>
|
||||||
|
</tbody>
|
||||||
|
{% endif %}
|
||||||
|
<tbody data-dnd-url="{% url "control:event.items.reorder" organizer=request.event.organizer.slug event=request.event.slug category=c.id|default:0 %}"
|
||||||
|
data-dnd-group="items">
|
||||||
|
{% for i in items %}
|
||||||
|
{% if forloop.counter0 == 0 and i.category %}{% endif %}
|
||||||
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
|
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
|
||||||
<td><strong>
|
<td><strong>
|
||||||
{% if not i.active %}<strike>{% endif %}
|
{% if not i.active %}<strike>{% endif %}
|
||||||
@@ -92,15 +102,15 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if i.var_count %}
|
{% if i.var_count %}
|
||||||
<span class="fa fa-th-large fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
|
<span class="fa fa-bars fa-fw text-muted" data-toggle="tooltip" title="{% trans "Product with variations" %}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if i.category.is_addon %}
|
{% if i.category.is_addon %}
|
||||||
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
|
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
|
||||||
title="{% trans "Only available as an add-on product" %}"></span>
|
title="{% trans "Only available as an add-on product" %}"></span>
|
||||||
{% elif i.require_bundling %}
|
{% elif i.require_bundling %}
|
||||||
<span class="fa fa-puzzle-piece fa-fw text-muted" data-toggle="tooltip"
|
<span class="fa fa-plus-square fa-fw text-muted" data-toggle="tooltip"
|
||||||
title="{% trans "Only available as part of a bundle" %}"></span>
|
title="{% trans "Only available as part of a bundle" %}"></span>
|
||||||
{% elif i.hide_without_voucher %}
|
{% elif i.hide_without_voucher %}
|
||||||
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
|
<span class="fa fa-tags fa-fw text-muted" data-toggle="tooltip"
|
||||||
@@ -110,16 +120,29 @@
|
|||||||
title="{% trans "Can only be bought using a voucher" %}"></span>
|
title="{% trans "Can only be bought using a voucher" %}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{% if i.category %}{{ i.category.name }}{% endif %}</td>
|
<td class="text-right flip">
|
||||||
<td>
|
{% if i.free_price %}
|
||||||
<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>
|
<span class="fa fa-edit fa-fw text-muted" data-toggle="tooltip" title="{% trans "Free price input" %}">
|
||||||
<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>
|
||||||
<span class="dnd-container"></span>
|
{% endif %}
|
||||||
|
{{ i.default_price|money:request.event.currency }}
|
||||||
|
{% if i.original_price %}<strike class="text-muted">{{ i.original_price|money:request.event.currency }}</strike>{% endif %}
|
||||||
|
{% if i.tax_rule and i.default_price %}
|
||||||
|
<br/>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% blocktrans trimmed with rate=i.tax_rule.rate|floatformat:-2 taxname=i.tax_rule.name|default:s_taxes %}
|
||||||
|
incl. {{ rate }}% {{ taxname }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right flip col-actions">
|
<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>
|
<button title="{% trans "Move up" %}" 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 title="{% trans "Move down" %}" 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" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||||
|
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
|
||||||
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
|
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
|
||||||
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -436,7 +436,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if line.used_membership %}
|
{% if line.used_membership %}
|
||||||
<br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span>
|
<br /><span class="fa fa-id-card fa-fw" aria-hidden="true"></span>
|
||||||
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=order.customer.identifier id=line.used_membership.pk %}">
|
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=line.used_membership.customer.identifier id=line.used_membership.pk %}">
|
||||||
{{ line.used_membership }}
|
{{ line.used_membership }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -192,6 +192,7 @@
|
|||||||
<input type="hidden" name="start-action" value="{{ start_form.cleaned_data.action }}">
|
<input type="hidden" name="start-action" value="{{ start_form.cleaned_data.action }}">
|
||||||
<input type="hidden" name="start-mode" value="{{ start_form.cleaned_data.mode }}">
|
<input type="hidden" name="start-mode" value="{{ start_form.cleaned_data.mode }}">
|
||||||
<input type="hidden" name="start-partial_amount" value="{{ partial_amount }}">
|
<input type="hidden" name="start-partial_amount" value="{{ partial_amount }}">
|
||||||
|
<input type="hidden" name="last_known_refund_id" value="{{ last_known_refund_id }}">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label" for="id_comment">{% trans "Refund reason" %}</label>
|
<label class="control-label" for="id_comment">{% trans "Refund reason" %}</label>
|
||||||
|
|||||||
@@ -51,9 +51,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.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 title="{% trans "Move up" %}" formaction="{% url "control:organizer.property.up" organizer=request.organizer.slug property=p.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:organizer.property.down" organizer=request.organizer.slug property=p.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>
|
<button title="{% trans "Move down" %}" formaction="{% url "control:organizer.property.down" organizer=request.organizer.slug property=p.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>
|
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right flip">
|
<td class="text-right flip">
|
||||||
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"
|
<a href="{% url "control:organizer.property.edit" organizer=request.organizer.slug property=p.id %}"
|
||||||
|
|||||||
@@ -173,6 +173,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right flip">
|
<td class="text-right flip">
|
||||||
|
<a href="{% url "control:event.orders" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}" class="btn btn-default btn-sm" title="{% trans "Show orders" %}"><i class="fa fa-shopping-cart" aria-hidden="true"></i></a>
|
||||||
|
|
||||||
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||||
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
|
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
|
||||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
||||||
|
|||||||
@@ -48,6 +48,12 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
id="id_url" readonly>
|
id="id_url" readonly>
|
||||||
<div class="input-group-btn">
|
<div class="input-group-btn">
|
||||||
|
<button type="button" class="btn btn-default btn-clipboard" data-clipboard-target="#id_url">
|
||||||
|
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||||
|
<span class="sr-only">
|
||||||
|
{% trans "Copy" %}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" title="{% trans "Create QR code" %}" aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="fa fa-qrcode" aria-hidden="true"></i>
|
<i class="fa fa-qrcode" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ urlpatterns = [
|
|||||||
re_path(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
|
re_path(r'^global/settings/$', global_settings.GlobalSettingsView.as_view(), name='global.settings'),
|
||||||
re_path(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
|
re_path(r'^global/update/$', global_settings.UpdateCheckView.as_view(), name='global.update'),
|
||||||
re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'),
|
re_path(r'^global/license/$', global_settings.LicenseCheckView.as_view(), name='global.license'),
|
||||||
|
re_path(r'^global/sysreport/$', global_settings.SysReportView.as_view(), name='global.sysreport'),
|
||||||
re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
|
re_path(r'^global/message/$', global_settings.MessageView.as_view(), name='global.message'),
|
||||||
re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
|
re_path(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
|
||||||
re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
|
re_path(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
|
||||||
@@ -247,6 +248,7 @@ urlpatterns = [
|
|||||||
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
|
re_path(r'^widgets.json$', dashboards.event_index_widgets_lazy, name='event.index.widgets'),
|
||||||
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
|
re_path(r'^logs/embed$', dashboards.event_index_log_lazy, name='event.index.logs'),
|
||||||
re_path(r'^live/$', event.EventLive.as_view(), name='event.live'),
|
re_path(r'^live/$', event.EventLive.as_view(), name='event.live'),
|
||||||
|
re_path(r'^transfer_session/$', event.EventTransferSession.as_view(), name='event.transfer_session'),
|
||||||
re_path(r'^logs/$', event.EventLog.as_view(), name='event.log'),
|
re_path(r'^logs/$', event.EventLog.as_view(), name='event.log'),
|
||||||
re_path(r'^delete/$', event.EventDelete.as_view(), name='event.delete'),
|
re_path(r'^delete/$', event.EventDelete.as_view(), name='event.delete'),
|
||||||
re_path(r'^comment/$', event.EventComment.as_view(),
|
re_path(r'^comment/$', event.EventComment.as_view(),
|
||||||
@@ -291,7 +293,7 @@ urlpatterns = [
|
|||||||
re_path(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
|
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+)/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/(?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/reorder/(?P<category>\d+)/$', 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/(?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/typeahead/meta/$', typeahead.item_meta_values, name='event.items.meta.typeahead'),
|
||||||
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),
|
re_path(r'^items/select2$', typeahead.items_select2, name='event.items.select2'),
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ from i18nfield.utils import I18nJSONEncoder
|
|||||||
|
|
||||||
from pretix.base.channels import get_all_sales_channels
|
from pretix.base.channels import get_all_sales_channels
|
||||||
from pretix.base.email import get_available_placeholders
|
from pretix.base.email import get_available_placeholders
|
||||||
|
from pretix.base.forms import PlaceholderValidator
|
||||||
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
|
from pretix.base.models import Event, LogEntry, Order, TaxRule, Voucher
|
||||||
from pretix.base.models.event import EventMetaValue
|
from pretix.base.models.event import EventMetaValue
|
||||||
from pretix.base.services import tickets
|
from pretix.base.services import tickets
|
||||||
@@ -93,13 +94,12 @@ from pretix.control.views.user import RecentAuthenticationRequiredMixin
|
|||||||
from pretix.helpers.database import rolledback_transaction
|
from pretix.helpers.database import rolledback_transaction
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
|
from pretix.multidomain.urlreverse import build_absolute_uri, get_event_domain
|
||||||
from pretix.plugins.stripe.payment import StripeSettingsHolder
|
from pretix.plugins.stripe.payment import StripeSettingsHolder
|
||||||
from pretix.presale.style import regenerate_css
|
|
||||||
|
|
||||||
from ...base.i18n import language
|
from ...base.i18n import language
|
||||||
from ...base.models.items import (
|
from ...base.models.items import (
|
||||||
Item, ItemCategory, ItemMetaProperty, Question, Quota,
|
Item, ItemCategory, ItemMetaProperty, Question, Quota,
|
||||||
)
|
)
|
||||||
from ...base.settings import SETTINGS_AFFECTING_CSS, LazyI18nStringList
|
from ...base.settings import LazyI18nStringList
|
||||||
from ...helpers.compat import CompatDeleteView
|
from ...helpers.compat import CompatDeleteView
|
||||||
from ...helpers.format import format_map
|
from ...helpers.format import format_map
|
||||||
from ..logdisplay import OVERVIEW_BANLIST
|
from ..logdisplay import OVERVIEW_BANLIST
|
||||||
@@ -201,19 +201,17 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self._save_decoupled(self.sform)
|
self._save_decoupled(self.sform)
|
||||||
self.sform.save()
|
self.sform.save()
|
||||||
|
self.object.cache.clear()
|
||||||
self.save_meta()
|
self.save_meta()
|
||||||
self.save_item_meta_property_formset(self.object)
|
self.save_item_meta_property_formset(self.object)
|
||||||
self.save_confirm_texts_formset(self.object)
|
self.save_confirm_texts_formset(self.object)
|
||||||
self.save_footer_links_formset(self.object)
|
self.save_footer_links_formset(self.object)
|
||||||
change_css = False
|
|
||||||
|
|
||||||
if self.sform.has_changed() or self.confirm_texts_formset.has_changed():
|
if self.sform.has_changed() or self.confirm_texts_formset.has_changed():
|
||||||
data = {k: self.request.event.settings.get(k) for k in self.sform.changed_data}
|
data = {k: self.request.event.settings.get(k) for k in self.sform.changed_data}
|
||||||
if self.confirm_texts_formset.has_changed():
|
if self.confirm_texts_formset.has_changed():
|
||||||
data.update(confirm_texts=self.confirm_texts_formset.cleaned_data)
|
data.update(confirm_texts=self.confirm_texts_formset.cleaned_data)
|
||||||
self.request.event.log_action('pretix.event.settings', user=self.request.user, data=data)
|
self.request.event.log_action('pretix.event.settings', user=self.request.user, data=data)
|
||||||
if any(p in self.sform.changed_data for p in SETTINGS_AFFECTING_CSS):
|
|
||||||
change_css = True
|
|
||||||
if self.footer_links_formset.has_changed():
|
if self.footer_links_formset.has_changed():
|
||||||
self.request.event.log_action('pretix.event.footerlinks.changed', user=self.request.user, data={
|
self.request.event.log_action('pretix.event.footerlinks.changed', user=self.request.user, data={
|
||||||
'data': self.footer_links_formset.cleaned_data
|
'data': self.footer_links_formset.cleaned_data
|
||||||
@@ -227,13 +225,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
|
|||||||
})
|
})
|
||||||
|
|
||||||
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk})
|
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk})
|
||||||
if change_css:
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
regenerate_css.apply_async(args=(self.request.event.pk,))
|
|
||||||
messages.success(self.request, _('Your changes have been saved. Please note that it can '
|
|
||||||
'take a short period of time until your changes become '
|
|
||||||
'active.'))
|
|
||||||
else:
|
|
||||||
messages.success(self.request, _('Your changes have been saved.'))
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
@@ -713,11 +705,6 @@ class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
|
|||||||
class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||||
permission = 'can_change_event_settings'
|
permission = 'can_change_event_settings'
|
||||||
|
|
||||||
# return the origin text if key is missing in dict
|
|
||||||
class SafeDict(dict):
|
|
||||||
def __missing__(self, key):
|
|
||||||
return '{' + key + '}'
|
|
||||||
|
|
||||||
# create index-language mapping
|
# create index-language mapping
|
||||||
@cached_property
|
@cached_property
|
||||||
def supported_locale(self):
|
def supported_locale(self):
|
||||||
@@ -742,7 +729,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
|||||||
_('This value will be replaced based on dynamic parameters.'),
|
_('This value will be replaced based on dynamic parameters.'),
|
||||||
s
|
s
|
||||||
)
|
)
|
||||||
return self.SafeDict(ctx)
|
return ctx
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
preview_item = request.POST.get('item', '')
|
preview_item = request.POST.get('item', '')
|
||||||
@@ -758,12 +745,21 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
|||||||
idx = matched.group('idx')
|
idx = matched.group('idx')
|
||||||
if idx in self.supported_locale:
|
if idx in self.supported_locale:
|
||||||
with language(self.supported_locale[idx], self.request.event.settings.region):
|
with language(self.supported_locale[idx], self.request.event.settings.region):
|
||||||
if k.startswith('mail_subject_'):
|
try:
|
||||||
msgs[self.supported_locale[idx]] = format_map(bleach.clean(v), self.placeholders(preview_item))
|
if k.startswith('mail_subject_'):
|
||||||
else:
|
msgs[self.supported_locale[idx]] = format_map(
|
||||||
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
bleach.clean(v), self.placeholders(preview_item), raise_on_missing=True
|
||||||
format_map(v, self.placeholders(preview_item))
|
)
|
||||||
)
|
else:
|
||||||
|
msgs[self.supported_locale[idx]] = markdown_compile_email(
|
||||||
|
format_map(v, self.placeholders(preview_item), raise_on_missing=True)
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||||
|
PlaceholderValidator.error_message)
|
||||||
|
except KeyError as e:
|
||||||
|
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||||
|
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'item': preview_item,
|
'item': preview_item,
|
||||||
@@ -1017,6 +1013,11 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class EventTransferSession(EventPermissionRequiredMixin, TemplateView):
|
||||||
|
permission = 'can_change_event_settings'
|
||||||
|
template_name = 'pretixcontrol/event/transfer_session.html'
|
||||||
|
|
||||||
|
|
||||||
class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView):
|
class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView):
|
||||||
permission = 'can_change_event_settings'
|
permission = 'can_change_event_settings'
|
||||||
template_name = 'pretixcontrol/event/delete.html'
|
template_name = 'pretixcontrol/event/delete.html'
|
||||||
|
|||||||
@@ -32,14 +32,16 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
import importlib_metadata as metadata
|
import importlib_metadata as metadata
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import JsonResponse
|
from django.http import HttpResponse, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import FormView, TemplateView
|
from django.views.generic import FormView, TemplateView
|
||||||
|
|
||||||
|
from pretix.base.i18n import language
|
||||||
from pretix.base.models import LogEntry, OrderPayment, OrderRefund
|
from pretix.base.models import LogEntry, OrderPayment, OrderRefund
|
||||||
from pretix.base.services.update_check import check_result_table, update_check
|
from pretix.base.services.update_check import check_result_table, update_check
|
||||||
from pretix.base.settings import GlobalSettingsObject
|
from pretix.base.settings import GlobalSettingsObject
|
||||||
@@ -49,6 +51,7 @@ from pretix.control.forms.global_settings import (
|
|||||||
from pretix.control.permissions import (
|
from pretix.control.permissions import (
|
||||||
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
AdministratorPermissionRequiredMixin, StaffMemberRequiredMixin,
|
||||||
)
|
)
|
||||||
|
from pretix.control.sysreport import SysReport
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
|
class GlobalSettingsView(AdministratorPermissionRequiredMixin, FormView):
|
||||||
@@ -262,3 +265,25 @@ class LicenseCheckView(StaffMemberRequiredMixin, FormView):
|
|||||||
))
|
))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
|
||||||
|
template_name = 'pretixcontrol/global_sysreport.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
with language("en"):
|
||||||
|
try:
|
||||||
|
month = int(request.POST.get("month"))
|
||||||
|
except ValueError:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
if month < 1 or month > 12:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
name, mime, data = SysReport(month, settings.TIME_ZONE).render()
|
||||||
|
resp = HttpResponse(data)
|
||||||
|
resp['Content-Type'] = mime
|
||||||
|
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
|
||||||
|
resp._csp_ignore = True
|
||||||
|
return resp
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
from collections import OrderedDict, namedtuple
|
from collections import OrderedDict, namedtuple
|
||||||
|
from itertools import groupby
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -113,6 +114,8 @@ class ItemList(ListView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
ctx['sales_channels'] = get_all_sales_channels()
|
ctx['sales_channels'] = get_all_sales_channels()
|
||||||
|
items_by_category = {cat: list(items) for cat, items in groupby(ctx['items'], lambda item: item.category)}
|
||||||
|
ctx['cat_list'] = [(cat, items_by_category.get(cat, [])) for cat in [None, *self.request.event.categories.all()]]
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
@@ -169,7 +172,7 @@ def item_move_down(request, organizer, event, item):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@event_permission_required("can_change_items")
|
@event_permission_required("can_change_items")
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
def reorder_items(request, organizer, event):
|
def reorder_items(request, organizer, event, category):
|
||||||
try:
|
try:
|
||||||
ids = json.loads(request.body.decode('utf-8'))['ids']
|
ids = json.loads(request.body.decode('utf-8'))['ids']
|
||||||
except (JSONDecodeError, KeyError, ValueError):
|
except (JSONDecodeError, KeyError, ValueError):
|
||||||
@@ -180,23 +183,21 @@ def reorder_items(request, organizer, event):
|
|||||||
if len(input_items) != len(ids):
|
if len(input_items) != len(ids):
|
||||||
raise Http404(_("Some of the provided object ids are invalid."))
|
raise Http404(_("Some of the provided object ids are invalid."))
|
||||||
|
|
||||||
item_categories = {i.category_id for i in input_items}
|
if int(category):
|
||||||
if len(item_categories) > 1:
|
target_category = request.event.categories.get(id=category)
|
||||||
raise Http404(_("You cannot reorder items spanning different categories."))
|
else:
|
||||||
|
target_category = None
|
||||||
# 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 objects have been selected."))
|
|
||||||
|
|
||||||
for i in input_items:
|
for i in input_items:
|
||||||
pos = ids.index(str(i.pk))
|
pos = ids.index(str(i.pk))
|
||||||
if pos != i.position: # Save unneccessary UPDATE queries
|
if pos != i.position or target_category != i.category: # Save unneccessary UPDATE queries
|
||||||
i.position = pos
|
i.position = pos
|
||||||
i.save(update_fields=['position'])
|
i.category = target_category
|
||||||
|
i.save(update_fields=['position', 'category_id'])
|
||||||
i.log_action(
|
i.log_action(
|
||||||
'pretix.event.item.reordered', user=request.user, data={
|
'pretix.event.item.reordered', user=request.user, data={
|
||||||
'position': i,
|
'position': i,
|
||||||
|
'category': target_category and target_category.pk,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1323,6 +1324,8 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
|||||||
def plugin_forms(self):
|
def plugin_forms(self):
|
||||||
forms = []
|
forms = []
|
||||||
for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
|
for rec, resp in item_forms.send(sender=self.request.event, item=self.item, request=self.request):
|
||||||
|
if not resp:
|
||||||
|
continue
|
||||||
if isinstance(resp, (list, tuple)):
|
if isinstance(resp, (list, tuple)):
|
||||||
forms.extend(resp)
|
forms.extend(resp)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+296
-276
@@ -49,7 +49,7 @@ from django.core.exceptions import PermissionDenied, ValidationError
|
|||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Count, Exists, F, IntegerField, OuterRef, Prefetch, ProtectedError, Q,
|
Count, Exists, F, IntegerField, Max, OuterRef, Prefetch, ProtectedError, Q,
|
||||||
QuerySet, Subquery, Sum,
|
QuerySet, Subquery, Sum,
|
||||||
)
|
)
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
@@ -129,6 +129,7 @@ from pretix.control.forms.rrule import RRuleForm
|
|||||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||||
from pretix.control.signals import order_search_forms
|
from pretix.control.signals import order_search_forms
|
||||||
from pretix.control.views import PaginationMixin
|
from pretix.control.views import PaginationMixin
|
||||||
|
from pretix.helpers import OF_SELF
|
||||||
from pretix.helpers.compat import CompatDeleteView
|
from pretix.helpers.compat import CompatDeleteView
|
||||||
from pretix.helpers.format import format_map
|
from pretix.helpers.format import format_map
|
||||||
from pretix.helpers.safedownload import check_token
|
from pretix.helpers.safedownload import check_token
|
||||||
@@ -1103,249 +1104,9 @@ class OrderRefundView(OrderView):
|
|||||||
p.propose_refund = proposals.get(p, 0)
|
p.propose_refund = proposals.get(p, 0)
|
||||||
|
|
||||||
if 'perform' in self.request.POST:
|
if 'perform' in self.request.POST:
|
||||||
refund_selected = Decimal('0.00')
|
r = self.perform_refund(comment, full_refund, payments)
|
||||||
refunds = []
|
if r:
|
||||||
|
return r
|
||||||
is_valid = True
|
|
||||||
manual_value = self.request.POST.get('refund-manual', '0') or '0'
|
|
||||||
manual_value = formats.sanitize_separators(manual_value)
|
|
||||||
try:
|
|
||||||
manual_value = Decimal(manual_value)
|
|
||||||
except (DecimalException, TypeError):
|
|
||||||
messages.error(self.request, _('You entered an invalid number.'))
|
|
||||||
is_valid = False
|
|
||||||
else:
|
|
||||||
refund_selected += manual_value
|
|
||||||
if manual_value:
|
|
||||||
refunds.append(OrderRefund(
|
|
||||||
order=self.order,
|
|
||||||
payment=None,
|
|
||||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
|
||||||
state=(
|
|
||||||
OrderRefund.REFUND_STATE_DONE
|
|
||||||
if self.request.POST.get('manual_state') == 'done'
|
|
||||||
else OrderRefund.REFUND_STATE_CREATED
|
|
||||||
),
|
|
||||||
execution_date=(
|
|
||||||
now()
|
|
||||||
if self.request.POST.get('manual_state') == 'done'
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
amount=manual_value,
|
|
||||||
comment=comment,
|
|
||||||
provider='manual'
|
|
||||||
))
|
|
||||||
|
|
||||||
giftcard_value = self.request.POST.get('refund-new-giftcard', '0') or '0'
|
|
||||||
giftcard_value = formats.sanitize_separators(giftcard_value)
|
|
||||||
try:
|
|
||||||
giftcard_value = Decimal(giftcard_value)
|
|
||||||
except (DecimalException, TypeError):
|
|
||||||
messages.error(self.request, _('You entered an invalid number.'))
|
|
||||||
is_valid = False
|
|
||||||
else:
|
|
||||||
if giftcard_value:
|
|
||||||
refund_selected += giftcard_value
|
|
||||||
|
|
||||||
if self.request.POST.get('giftcard-expires'):
|
|
||||||
try:
|
|
||||||
expires = forms.DateField().to_python(self.request.POST.get('giftcard-expires'))
|
|
||||||
expires = make_aware(datetime.combine(
|
|
||||||
expires,
|
|
||||||
time(hour=23, minute=59, second=59)
|
|
||||||
), self.request.event.timezone)
|
|
||||||
except ValidationError as e:
|
|
||||||
messages.error(self.request, e.message)
|
|
||||||
is_valid = False
|
|
||||||
else:
|
|
||||||
expires = None
|
|
||||||
|
|
||||||
giftcard = self.request.organizer.issued_gift_cards.create(
|
|
||||||
expires=expires,
|
|
||||||
currency=self.request.event.currency,
|
|
||||||
testmode=self.order.testmode
|
|
||||||
)
|
|
||||||
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
|
||||||
refunds.append(OrderRefund(
|
|
||||||
order=self.order,
|
|
||||||
payment=None,
|
|
||||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
|
||||||
state=OrderRefund.REFUND_STATE_CREATED,
|
|
||||||
execution_date=now(),
|
|
||||||
amount=giftcard_value,
|
|
||||||
provider='giftcard',
|
|
||||||
comment=comment,
|
|
||||||
info=json.dumps({
|
|
||||||
'gift_card': giftcard.pk
|
|
||||||
})
|
|
||||||
))
|
|
||||||
|
|
||||||
offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0'
|
|
||||||
offsetting_value = formats.sanitize_separators(offsetting_value)
|
|
||||||
try:
|
|
||||||
offsetting_value = Decimal(offsetting_value)
|
|
||||||
except (DecimalException, TypeError):
|
|
||||||
messages.error(self.request, _('You entered an invalid number.'))
|
|
||||||
is_valid = False
|
|
||||||
else:
|
|
||||||
if offsetting_value:
|
|
||||||
refund_selected += offsetting_value
|
|
||||||
try:
|
|
||||||
order = Order.objects.get(code=self.request.POST.get('order-offsetting'),
|
|
||||||
event__organizer=self.request.organizer)
|
|
||||||
except Order.DoesNotExist:
|
|
||||||
messages.error(self.request, _('You entered an order that could not be found.'))
|
|
||||||
is_valid = False
|
|
||||||
else:
|
|
||||||
if order.event.currency != self.request.event.currency:
|
|
||||||
messages.error(self.request, _('You entered an order in an event with a different currency.'))
|
|
||||||
is_valid = False
|
|
||||||
refunds.append(OrderRefund(
|
|
||||||
order=self.order,
|
|
||||||
payment=None,
|
|
||||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
|
||||||
state=OrderRefund.REFUND_STATE_DONE,
|
|
||||||
execution_date=now(),
|
|
||||||
amount=offsetting_value,
|
|
||||||
provider='offsetting',
|
|
||||||
comment=comment,
|
|
||||||
info=json.dumps({
|
|
||||||
'orders': [order.code]
|
|
||||||
})
|
|
||||||
))
|
|
||||||
|
|
||||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
|
||||||
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
|
|
||||||
prof_value = formats.sanitize_separators(prof_value)
|
|
||||||
try:
|
|
||||||
prof_value = Decimal(prof_value)
|
|
||||||
except (DecimalException, TypeError):
|
|
||||||
messages.error(self.request, _('You entered an invalid number.'))
|
|
||||||
is_valid = False
|
|
||||||
continue
|
|
||||||
if prof_value > Decimal('0.00'):
|
|
||||||
try:
|
|
||||||
refund = prov.new_refund_control_form_process(self.request, prof_value, self.order)
|
|
||||||
except ValidationError as e:
|
|
||||||
for err in e:
|
|
||||||
messages.error(self.request, err)
|
|
||||||
is_valid = False
|
|
||||||
continue
|
|
||||||
if refund:
|
|
||||||
refund_selected += refund.amount
|
|
||||||
refund.comment = comment
|
|
||||||
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
|
|
||||||
refunds.append(refund)
|
|
||||||
|
|
||||||
for p in payments:
|
|
||||||
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
|
|
||||||
value = formats.sanitize_separators(value)
|
|
||||||
try:
|
|
||||||
value = Decimal(value)
|
|
||||||
except (DecimalException, TypeError):
|
|
||||||
messages.error(self.request, _('You entered an invalid number.'))
|
|
||||||
is_valid = False
|
|
||||||
else:
|
|
||||||
if value == 0:
|
|
||||||
continue
|
|
||||||
elif value > p.available_amount:
|
|
||||||
messages.error(self.request, _('You can not refund more than the amount of a '
|
|
||||||
'payment that is not yet refunded.'))
|
|
||||||
is_valid = False
|
|
||||||
break
|
|
||||||
elif value != p.amount and not p.partial_refund_possible:
|
|
||||||
messages.error(self.request, _('You selected a partial refund for a payment method that '
|
|
||||||
'only supports full refunds.'))
|
|
||||||
is_valid = False
|
|
||||||
break
|
|
||||||
elif (p.partial_refund_possible or p.full_refund_possible) and value > 0:
|
|
||||||
refund_selected += value
|
|
||||||
refunds.append(OrderRefund(
|
|
||||||
order=self.order,
|
|
||||||
payment=p,
|
|
||||||
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
|
||||||
state=OrderRefund.REFUND_STATE_CREATED,
|
|
||||||
amount=value,
|
|
||||||
comment=comment,
|
|
||||||
provider=p.provider
|
|
||||||
))
|
|
||||||
|
|
||||||
any_success = False
|
|
||||||
if refund_selected == full_refund and is_valid:
|
|
||||||
for r in refunds:
|
|
||||||
r.save()
|
|
||||||
self.order.log_action('pretix.event.order.refund.created', {
|
|
||||||
'local_id': r.local_id,
|
|
||||||
'provider': r.provider,
|
|
||||||
}, user=self.request.user)
|
|
||||||
if r.provider != "manual":
|
|
||||||
try:
|
|
||||||
r.payment_provider.execute_refund(r)
|
|
||||||
except PaymentException as e:
|
|
||||||
r.state = OrderRefund.REFUND_STATE_FAILED
|
|
||||||
r.save()
|
|
||||||
messages.error(self.request, _('One of the refunds failed to be processed. You should '
|
|
||||||
'retry to refund in a different way. The error message '
|
|
||||||
'was: {}').format(str(e)))
|
|
||||||
else:
|
|
||||||
any_success = True
|
|
||||||
if r.state == OrderRefund.REFUND_STATE_DONE:
|
|
||||||
messages.success(self.request, _('A refund of {} has been processed.').format(
|
|
||||||
money_filter(r.amount, self.request.event.currency)
|
|
||||||
))
|
|
||||||
elif r.state == OrderRefund.REFUND_STATE_CREATED:
|
|
||||||
messages.info(self.request, _('A refund of {} has been saved, but not yet '
|
|
||||||
'fully executed. You can mark it as complete '
|
|
||||||
'below.').format(
|
|
||||||
money_filter(r.amount, self.request.event.currency)
|
|
||||||
))
|
|
||||||
else:
|
|
||||||
any_success = True
|
|
||||||
|
|
||||||
if r.state == OrderRefund.REFUND_STATE_DONE:
|
|
||||||
self.order.log_action('pretix.event.order.refund.done', {
|
|
||||||
'local_id': r.local_id,
|
|
||||||
'provider': r.provider,
|
|
||||||
}, user=self.request.user)
|
|
||||||
|
|
||||||
if any_success:
|
|
||||||
if self.start_form.cleaned_data.get('action') == 'mark_refunded':
|
|
||||||
if self.order.cancel_allowed():
|
|
||||||
mark_order_refunded(self.order, user=self.request.user)
|
|
||||||
elif self.start_form.cleaned_data.get('action') == 'mark_pending':
|
|
||||||
if not (self.order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
|
|
||||||
self.order.status = Order.STATUS_PENDING
|
|
||||||
self.order.set_expires(
|
|
||||||
now(),
|
|
||||||
self.order.event.subevents.filter(
|
|
||||||
id__in=self.order.positions.values_list('subevent_id', flat=True))
|
|
||||||
)
|
|
||||||
self.order.save(update_fields=['status', 'expires'])
|
|
||||||
|
|
||||||
if giftcard_value and self.order.email:
|
|
||||||
messages.success(self.request, _('A new gift card was created. You can now send the user their '
|
|
||||||
'gift card code.'))
|
|
||||||
with language(self.order.locale, self.request.event.settings.region):
|
|
||||||
return redirect(reverse('control:event.order.sendmail', kwargs={
|
|
||||||
'event': self.request.event.slug,
|
|
||||||
'organizer': self.request.event.organizer.slug,
|
|
||||||
'code': self.order.code
|
|
||||||
}) + '?' + urlencode({
|
|
||||||
'subject': gettext('Your gift card code'),
|
|
||||||
'message': gettext(
|
|
||||||
'Hello,\n\nwe have refunded you {amount} for your order.\n\nYou can use the gift '
|
|
||||||
'card code {giftcard} to pay for future ticket purchases in our shop.\n\n'
|
|
||||||
'Your {event} team'
|
|
||||||
).format(
|
|
||||||
event="{event}",
|
|
||||||
amount=money_filter(giftcard_value, self.request.event.currency),
|
|
||||||
giftcard=giftcard.secret,
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
return redirect(self.get_order_url())
|
|
||||||
else:
|
|
||||||
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
|
|
||||||
'amount.'))
|
|
||||||
|
|
||||||
new_refunds = []
|
new_refunds = []
|
||||||
for identifier, prov in self.request.event.get_payment_providers().items():
|
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||||
@@ -1375,9 +1136,264 @@ class OrderRefundView(OrderView):
|
|||||||
self.request.POST.get('start-partial_amount') if self.request.method == 'POST'
|
self.request.POST.get('start-partial_amount') if self.request.method == 'POST'
|
||||||
else self.request.GET.get('start-partial_amount')
|
else self.request.GET.get('start-partial_amount')
|
||||||
),
|
),
|
||||||
'start_form': self.start_form
|
'start_form': self.start_form,
|
||||||
|
'last_known_refund_id': self.order.refunds.aggregate(m=Max("id"))["m"] or 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@transaction.atomic()
|
||||||
|
def perform_refund(self, comment, full_refund, payments):
|
||||||
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
|
||||||
|
|
||||||
|
if self.request.POST.get("last_known_refund_id", "0") != str(self.order.refunds.aggregate(m=Max("id"))["m"] or 0):
|
||||||
|
messages.error(self.request, _('The refund was prevented due to a refund already being processed at the '
|
||||||
|
'same time. Please have a look at the order details and check if your '
|
||||||
|
'refund is still necessary.'))
|
||||||
|
return redirect(self.get_order_url())
|
||||||
|
|
||||||
|
refund_selected = Decimal('0.00')
|
||||||
|
refunds = []
|
||||||
|
|
||||||
|
is_valid = True
|
||||||
|
manual_value = self.request.POST.get('refund-manual', '0') or '0'
|
||||||
|
manual_value = formats.sanitize_separators(manual_value)
|
||||||
|
try:
|
||||||
|
manual_value = Decimal(manual_value)
|
||||||
|
except (DecimalException, TypeError):
|
||||||
|
messages.error(self.request, _('You entered an invalid number.'))
|
||||||
|
is_valid = False
|
||||||
|
else:
|
||||||
|
refund_selected += manual_value
|
||||||
|
if manual_value:
|
||||||
|
refunds.append(OrderRefund(
|
||||||
|
order=order,
|
||||||
|
payment=None,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||||
|
state=(
|
||||||
|
OrderRefund.REFUND_STATE_DONE
|
||||||
|
if self.request.POST.get('manual_state') == 'done'
|
||||||
|
else OrderRefund.REFUND_STATE_CREATED
|
||||||
|
),
|
||||||
|
execution_date=(
|
||||||
|
now()
|
||||||
|
if self.request.POST.get('manual_state') == 'done'
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
amount=manual_value,
|
||||||
|
comment=comment,
|
||||||
|
provider='manual'
|
||||||
|
))
|
||||||
|
|
||||||
|
giftcard_value = self.request.POST.get('refund-new-giftcard', '0') or '0'
|
||||||
|
giftcard_value = formats.sanitize_separators(giftcard_value)
|
||||||
|
try:
|
||||||
|
giftcard_value = Decimal(giftcard_value)
|
||||||
|
except (DecimalException, TypeError):
|
||||||
|
messages.error(self.request, _('You entered an invalid number.'))
|
||||||
|
is_valid = False
|
||||||
|
else:
|
||||||
|
if giftcard_value:
|
||||||
|
refund_selected += giftcard_value
|
||||||
|
|
||||||
|
if self.request.POST.get('giftcard-expires'):
|
||||||
|
try:
|
||||||
|
expires = forms.DateField().to_python(self.request.POST.get('giftcard-expires'))
|
||||||
|
expires = make_aware(datetime.combine(
|
||||||
|
expires,
|
||||||
|
time(hour=23, minute=59, second=59)
|
||||||
|
), self.request.event.timezone)
|
||||||
|
except ValidationError as e:
|
||||||
|
messages.error(self.request, e.message)
|
||||||
|
is_valid = False
|
||||||
|
else:
|
||||||
|
expires = None
|
||||||
|
|
||||||
|
giftcard = self.request.organizer.issued_gift_cards.create(
|
||||||
|
expires=expires,
|
||||||
|
currency=self.request.event.currency,
|
||||||
|
testmode=order.testmode
|
||||||
|
)
|
||||||
|
giftcard.log_action('pretix.giftcards.created', user=self.request.user, data={})
|
||||||
|
refunds.append(OrderRefund(
|
||||||
|
order=order,
|
||||||
|
payment=None,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||||
|
state=OrderRefund.REFUND_STATE_CREATED,
|
||||||
|
execution_date=now(),
|
||||||
|
amount=giftcard_value,
|
||||||
|
provider='giftcard',
|
||||||
|
comment=comment,
|
||||||
|
info=json.dumps({
|
||||||
|
'gift_card': giftcard.pk
|
||||||
|
})
|
||||||
|
))
|
||||||
|
|
||||||
|
offsetting_value = self.request.POST.get('refund-offsetting', '0') or '0'
|
||||||
|
offsetting_value = formats.sanitize_separators(offsetting_value)
|
||||||
|
try:
|
||||||
|
offsetting_value = Decimal(offsetting_value)
|
||||||
|
except (DecimalException, TypeError):
|
||||||
|
messages.error(self.request, _('You entered an invalid number.'))
|
||||||
|
is_valid = False
|
||||||
|
else:
|
||||||
|
if offsetting_value:
|
||||||
|
refund_selected += offsetting_value
|
||||||
|
try:
|
||||||
|
offset_order = Order.objects.get(code=self.request.POST.get('order-offsetting'),
|
||||||
|
event__organizer=self.request.organizer)
|
||||||
|
except Order.DoesNotExist:
|
||||||
|
messages.error(self.request, _('You entered an order that could not be found.'))
|
||||||
|
is_valid = False
|
||||||
|
else:
|
||||||
|
if offset_order.event.currency != self.request.event.currency:
|
||||||
|
messages.error(self.request, _('You entered an order in an event with a different currency.'))
|
||||||
|
is_valid = False
|
||||||
|
refunds.append(OrderRefund(
|
||||||
|
order=order,
|
||||||
|
payment=None,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||||
|
state=OrderRefund.REFUND_STATE_DONE,
|
||||||
|
execution_date=now(),
|
||||||
|
amount=offsetting_value,
|
||||||
|
provider='offsetting',
|
||||||
|
comment=comment,
|
||||||
|
info=json.dumps({
|
||||||
|
'orders': [offset_order.code]
|
||||||
|
})
|
||||||
|
))
|
||||||
|
|
||||||
|
for identifier, prov in self.request.event.get_payment_providers().items():
|
||||||
|
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
|
||||||
|
prof_value = formats.sanitize_separators(prof_value)
|
||||||
|
try:
|
||||||
|
prof_value = Decimal(prof_value)
|
||||||
|
except (DecimalException, TypeError):
|
||||||
|
messages.error(self.request, _('You entered an invalid number.'))
|
||||||
|
is_valid = False
|
||||||
|
continue
|
||||||
|
if prof_value > Decimal('0.00'):
|
||||||
|
try:
|
||||||
|
refund = prov.new_refund_control_form_process(self.request, prof_value, order)
|
||||||
|
except ValidationError as e:
|
||||||
|
for err in e:
|
||||||
|
messages.error(self.request, err)
|
||||||
|
is_valid = False
|
||||||
|
continue
|
||||||
|
if refund:
|
||||||
|
refund_selected += refund.amount
|
||||||
|
refund.comment = comment
|
||||||
|
refund.source = OrderRefund.REFUND_SOURCE_ADMIN
|
||||||
|
refunds.append(refund)
|
||||||
|
|
||||||
|
for p in payments:
|
||||||
|
value = self.request.POST.get('refund-{}'.format(p.pk), '0') or '0'
|
||||||
|
value = formats.sanitize_separators(value)
|
||||||
|
try:
|
||||||
|
value = Decimal(value)
|
||||||
|
except (DecimalException, TypeError):
|
||||||
|
messages.error(self.request, _('You entered an invalid number.'))
|
||||||
|
is_valid = False
|
||||||
|
else:
|
||||||
|
if value == 0:
|
||||||
|
continue
|
||||||
|
elif value > p.available_amount:
|
||||||
|
messages.error(self.request, _('You can not refund more than the amount of a '
|
||||||
|
'payment that is not yet refunded.'))
|
||||||
|
is_valid = False
|
||||||
|
break
|
||||||
|
elif value != p.amount and not p.partial_refund_possible:
|
||||||
|
messages.error(self.request, _('You selected a partial refund for a payment method that '
|
||||||
|
'only supports full refunds.'))
|
||||||
|
is_valid = False
|
||||||
|
break
|
||||||
|
elif (p.partial_refund_possible or p.full_refund_possible) and value > 0:
|
||||||
|
refund_selected += value
|
||||||
|
refunds.append(OrderRefund(
|
||||||
|
order=order,
|
||||||
|
payment=p,
|
||||||
|
source=OrderRefund.REFUND_SOURCE_ADMIN,
|
||||||
|
state=OrderRefund.REFUND_STATE_CREATED,
|
||||||
|
amount=value,
|
||||||
|
comment=comment,
|
||||||
|
provider=p.provider
|
||||||
|
))
|
||||||
|
|
||||||
|
any_success = False
|
||||||
|
if refund_selected == full_refund and is_valid:
|
||||||
|
for r in refunds:
|
||||||
|
r.save()
|
||||||
|
order.log_action('pretix.event.order.refund.created', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
}, user=self.request.user)
|
||||||
|
if r.provider != "manual":
|
||||||
|
try:
|
||||||
|
r.payment_provider.execute_refund(r)
|
||||||
|
except PaymentException as e:
|
||||||
|
r.state = OrderRefund.REFUND_STATE_FAILED
|
||||||
|
r.save()
|
||||||
|
messages.error(self.request, _('One of the refunds failed to be processed. You should '
|
||||||
|
'retry to refund in a different way. The error message '
|
||||||
|
'was: {}').format(str(e)))
|
||||||
|
else:
|
||||||
|
any_success = True
|
||||||
|
if r.state == OrderRefund.REFUND_STATE_DONE:
|
||||||
|
messages.success(self.request, _('A refund of {} has been processed.').format(
|
||||||
|
money_filter(r.amount, self.request.event.currency)
|
||||||
|
))
|
||||||
|
elif r.state == OrderRefund.REFUND_STATE_CREATED:
|
||||||
|
messages.info(self.request, _('A refund of {} has been saved, but not yet '
|
||||||
|
'fully executed. You can mark it as complete '
|
||||||
|
'below.').format(
|
||||||
|
money_filter(r.amount, self.request.event.currency)
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
any_success = True
|
||||||
|
|
||||||
|
if r.state == OrderRefund.REFUND_STATE_DONE:
|
||||||
|
order.log_action('pretix.event.order.refund.done', {
|
||||||
|
'local_id': r.local_id,
|
||||||
|
'provider': r.provider,
|
||||||
|
}, user=self.request.user)
|
||||||
|
|
||||||
|
if any_success:
|
||||||
|
if self.start_form.cleaned_data.get('action') == 'mark_refunded':
|
||||||
|
if order.cancel_allowed():
|
||||||
|
mark_order_refunded(order, user=self.request.user)
|
||||||
|
elif self.start_form.cleaned_data.get('action') == 'mark_pending':
|
||||||
|
if not (order.status == Order.STATUS_PAID and self.order.pending_sum <= 0):
|
||||||
|
order.status = Order.STATUS_PENDING
|
||||||
|
order.set_expires(
|
||||||
|
now(),
|
||||||
|
order.event.subevents.filter(
|
||||||
|
id__in=order.positions.values_list('subevent_id', flat=True))
|
||||||
|
)
|
||||||
|
order.save(update_fields=['status', 'expires'])
|
||||||
|
|
||||||
|
if giftcard_value and order.email:
|
||||||
|
messages.success(self.request, _('A new gift card was created. You can now send the user their '
|
||||||
|
'gift card code.'))
|
||||||
|
with language(order.locale, self.request.event.settings.region):
|
||||||
|
return redirect(reverse('control:event.order.sendmail', kwargs={
|
||||||
|
'event': self.request.event.slug,
|
||||||
|
'organizer': self.request.event.organizer.slug,
|
||||||
|
'code': order.code
|
||||||
|
}) + '?' + urlencode({
|
||||||
|
'subject': gettext('Your gift card code'),
|
||||||
|
'message': gettext(
|
||||||
|
'Hello,\n\nwe have refunded you {amount} for your order.\n\nYou can use the gift '
|
||||||
|
'card code {giftcard} to pay for future ticket purchases in our shop.\n\n'
|
||||||
|
'Your {event} team'
|
||||||
|
).format(
|
||||||
|
event="{event}",
|
||||||
|
amount=money_filter(giftcard_value, self.request.event.currency),
|
||||||
|
giftcard=giftcard.secret,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
return redirect(self.get_order_url())
|
||||||
|
else:
|
||||||
|
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
|
||||||
|
'amount.'))
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
def post(self, *args, **kwargs):
|
||||||
if self.start_form.is_valid():
|
if self.start_form.is_valid():
|
||||||
return self.choose_form()
|
return self.choose_form()
|
||||||
@@ -1562,20 +1578,22 @@ class OrderInvoiceCreate(OrderView):
|
|||||||
permission = 'can_change_orders'
|
permission = 'can_change_orders'
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
def post(self, *args, **kwargs):
|
||||||
has_inv = self.order.invoices.exists() and not (
|
with transaction.atomic():
|
||||||
self.order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
|
||||||
and self.order.invoices.filter(is_cancellation=True).count() >= self.order.invoices.filter(is_cancellation=False).count()
|
has_inv = order.invoices.exists() and not (
|
||||||
)
|
order.status in (Order.STATUS_PAID, Order.STATUS_PENDING)
|
||||||
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(self.order):
|
and order.invoices.filter(is_cancellation=True).count() >= order.invoices.filter(is_cancellation=False).count()
|
||||||
messages.error(self.request, _('You cannot generate an invoice for this order.'))
|
)
|
||||||
elif has_inv:
|
if self.request.event.settings.get('invoice_generate') not in ('admin', 'user', 'paid', 'True') or not invoice_qualified(order):
|
||||||
messages.error(self.request, _('An invoice for this order already exists.'))
|
messages.error(self.request, _('You cannot generate an invoice for this order.'))
|
||||||
else:
|
elif has_inv:
|
||||||
inv = generate_invoice(self.order)
|
messages.error(self.request, _('An invoice for this order already exists.'))
|
||||||
self.order.log_action('pretix.event.order.invoice.generated', user=self.request.user, data={
|
else:
|
||||||
'invoice': inv.pk
|
inv = generate_invoice(order)
|
||||||
})
|
order.log_action('pretix.event.order.invoice.generated', user=self.request.user, data={
|
||||||
messages.success(self.request, _('The invoice has been generated.'))
|
'invoice': inv.pk
|
||||||
|
})
|
||||||
|
messages.success(self.request, _('The invoice has been generated.'))
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
@@ -1657,25 +1675,27 @@ class OrderInvoiceReissue(OrderView):
|
|||||||
permission = 'can_change_orders'
|
permission = 'can_change_orders'
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
def post(self, *args, **kwargs):
|
||||||
try:
|
with transaction.atomic():
|
||||||
inv = self.order.invoices.get(pk=kwargs.get('id'))
|
order = Order.objects.select_for_update(of=OF_SELF).get(pk=self.order.pk)
|
||||||
except Invoice.DoesNotExist:
|
try:
|
||||||
messages.error(self.request, _('Unknown invoice.'))
|
inv = order.invoices.get(pk=kwargs.get('id'))
|
||||||
else:
|
except Invoice.DoesNotExist:
|
||||||
if inv.canceled:
|
messages.error(self.request, _('Unknown invoice.'))
|
||||||
messages.error(self.request, _('The invoice has already been canceled.'))
|
|
||||||
elif inv.shredded:
|
|
||||||
messages.error(self.request, _('The invoice has been cleaned of personal data.'))
|
|
||||||
else:
|
else:
|
||||||
c = generate_cancellation(inv)
|
if inv.canceled:
|
||||||
if self.order.status not in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
|
messages.error(self.request, _('The invoice has already been canceled.'))
|
||||||
inv = generate_invoice(self.order)
|
elif inv.shredded:
|
||||||
|
messages.error(self.request, _('The invoice has been cleaned of personal data.'))
|
||||||
else:
|
else:
|
||||||
inv = c
|
c = generate_cancellation(inv)
|
||||||
self.order.log_action('pretix.event.order.invoice.reissued', user=self.request.user, data={
|
if order.status not in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
|
||||||
'invoice': inv.pk
|
inv = generate_invoice(order)
|
||||||
})
|
else:
|
||||||
messages.success(self.request, _('The invoice has been reissued.'))
|
inv = c
|
||||||
|
order.log_action('pretix.event.order.invoice.reissued', user=self.request.user, data={
|
||||||
|
'invoice': inv.pk
|
||||||
|
})
|
||||||
|
messages.success(self.request, _('The invoice has been reissued.'))
|
||||||
return redirect(self.get_order_url())
|
return redirect(self.get_order_url())
|
||||||
|
|
||||||
def get(self, *args, **kwargs): # NOQA
|
def get(self, *args, **kwargs): # NOQA
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ from pretix.base.models.organizer import TeamAPIToken
|
|||||||
from pretix.base.payment import PaymentException
|
from pretix.base.payment import PaymentException
|
||||||
from pretix.base.services.export import multiexport, scheduled_organizer_export
|
from pretix.base.services.export import multiexport, scheduled_organizer_export
|
||||||
from pretix.base.services.mail import SendMailException, mail
|
from pretix.base.services.mail import SendMailException, mail
|
||||||
from pretix.base.settings import SETTINGS_AFFECTING_CSS
|
|
||||||
from pretix.base.signals import register_multievent_data_exporters
|
from pretix.base.signals import register_multievent_data_exporters
|
||||||
from pretix.base.templatetags.rich_text import markdown_compile_email
|
from pretix.base.templatetags.rich_text import markdown_compile_email
|
||||||
from pretix.base.views.tasks import AsyncAction
|
from pretix.base.views.tasks import AsyncAction
|
||||||
@@ -127,7 +126,6 @@ from pretix.helpers.format import format_map
|
|||||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||||
from pretix.presale.forms.customer import TokenGenerator
|
from pretix.presale.forms.customer import TokenGenerator
|
||||||
from pretix.presale.style import regenerate_organizer_css
|
|
||||||
|
|
||||||
|
|
||||||
class OrganizerList(PaginationMixin, ListView):
|
class OrganizerList(PaginationMixin, ListView):
|
||||||
@@ -466,7 +464,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.sform.save()
|
self.sform.save()
|
||||||
self.save_footer_links_formset(self.object)
|
self.save_footer_links_formset(self.object)
|
||||||
change_css = False
|
self.object.cache.clear()
|
||||||
if self.sform.has_changed():
|
if self.sform.has_changed():
|
||||||
self.request.organizer.log_action(
|
self.request.organizer.log_action(
|
||||||
'pretix.organizer.settings',
|
'pretix.organizer.settings',
|
||||||
@@ -478,8 +476,6 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
|||||||
for k in self.sform.changed_data
|
for k in self.sform.changed_data
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if any(p in self.sform.changed_data for p in SETTINGS_AFFECTING_CSS):
|
|
||||||
change_css = True
|
|
||||||
if self.footer_links_formset.has_changed():
|
if self.footer_links_formset.has_changed():
|
||||||
self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={
|
self.request.organizer.log_action('pretix.organizer.footerlinks.changed', user=self.request.user, data={
|
||||||
'data': self.footer_links_formset.cleaned_data
|
'data': self.footer_links_formset.cleaned_data
|
||||||
@@ -491,13 +487,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
|
|||||||
data={k: form.cleaned_data.get(k) for k in form.changed_data}
|
data={k: form.cleaned_data.get(k) for k in form.changed_data}
|
||||||
)
|
)
|
||||||
|
|
||||||
if change_css:
|
messages.success(self.request, _('Your changes have been saved.'))
|
||||||
regenerate_organizer_css.apply_async(args=(self.request.organizer.pk,))
|
|
||||||
messages.success(self.request, _('Your changes have been saved. Please note that it can '
|
|
||||||
'take a short period of time until your changes become '
|
|
||||||
'active.'))
|
|
||||||
else:
|
|
||||||
messages.success(self.request, _('Your changes have been saved.'))
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ def casual_reads():
|
|||||||
|
|
||||||
class GroupConcat(Aggregate):
|
class GroupConcat(Aggregate):
|
||||||
function = 'group_concat'
|
function = 'group_concat'
|
||||||
template = '%(function)s(%(field)s, "%(separator)s")'
|
template = '%(function)s(%(distinct)s%(field)s, "%(separator)s")'
|
||||||
|
allow_distinct = True
|
||||||
|
|
||||||
def __init__(self, *expressions, ordered=False, **extra):
|
def __init__(self, *expressions, ordered=False, **extra):
|
||||||
self.ordered = ordered
|
self.ordered = ordered
|
||||||
@@ -73,19 +74,17 @@ class GroupConcat(Aggregate):
|
|||||||
extra.update({'separator': ','})
|
extra.update({'separator': ','})
|
||||||
super().__init__(*expressions, **extra)
|
super().__init__(*expressions, **extra)
|
||||||
|
|
||||||
def as_postgresql(self, compiler, connection):
|
def as_postgresql(self, compiler, connection, **extra_context):
|
||||||
if self.ordered:
|
if self.ordered:
|
||||||
return super().as_sql(
|
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)"
|
||||||
compiler, connection,
|
|
||||||
function='string_agg',
|
|
||||||
template="%(function)s(%(field)s::text, '%(separator)s' ORDER BY %(field)s ASC)",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return super().as_sql(
|
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')"
|
||||||
compiler, connection,
|
return super().as_sql(
|
||||||
function='string_agg',
|
compiler, connection,
|
||||||
template="%(function)s(%(field)s::text, '%(separator)s')",
|
function='string_agg',
|
||||||
)
|
template=template,
|
||||||
|
**extra_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ReplicaRouter:
|
class ReplicaRouter:
|
||||||
|
|||||||
@@ -30,17 +30,15 @@ class SafeFormatter(Formatter):
|
|||||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||||
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
|
(b) does not allow any unwanted shenanigans like attribute access or format specifiers.
|
||||||
"""
|
"""
|
||||||
def __init__(self, context):
|
def __init__(self, context, raise_on_missing=False):
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.raise_on_missing = raise_on_missing
|
||||||
|
|
||||||
def get_field(self, field_name, args, kwargs):
|
def get_field(self, field_name, args, kwargs):
|
||||||
if '.' in field_name or '[' in field_name:
|
return self.get_value(field_name, args, kwargs), field_name
|
||||||
logger.warning(f'Ignored invalid field name "{field_name}"')
|
|
||||||
return ('{' + str(field_name) + '}', field_name)
|
|
||||||
return super().get_field(field_name, args, kwargs)
|
|
||||||
|
|
||||||
def get_value(self, key, args, kwargs):
|
def get_value(self, key, args, kwargs):
|
||||||
if key not in self.context:
|
if not self.raise_on_missing and key not in self.context:
|
||||||
return '{' + str(key) + '}'
|
return '{' + str(key) + '}'
|
||||||
return self.context[key]
|
return self.context[key]
|
||||||
|
|
||||||
@@ -49,7 +47,7 @@ class SafeFormatter(Formatter):
|
|||||||
return super().format_field(value, '')
|
return super().format_field(value, '')
|
||||||
|
|
||||||
|
|
||||||
def format_map(template, context):
|
def format_map(template, context, raise_on_missing=False):
|
||||||
if not isinstance(template, str):
|
if not isinstance(template, str):
|
||||||
template = str(template)
|
template = str(template)
|
||||||
return SafeFormatter(context).format(template)
|
return SafeFormatter(context, raise_on_missing).format(template)
|
||||||
|
|||||||
@@ -72,9 +72,13 @@ def remove_invalid_excel_chars(val):
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def SafeCell(*args, value=None, **kwargs):
|
def SafeCell(worksheet, row=None, column=None, value=None, **kwargs):
|
||||||
value = remove_invalid_excel_chars(value)
|
value = remove_invalid_excel_chars(value)
|
||||||
c = Cell(*args, value=value, **kwargs)
|
if not column:
|
||||||
|
column = 1
|
||||||
|
if not row:
|
||||||
|
row = 1
|
||||||
|
c = Cell(worksheet, row=row, column=column, value=value, **kwargs)
|
||||||
if c.data_type == TYPE_FORMULA:
|
if c.data_type == TYPE_FORMULA:
|
||||||
c.data_type = TYPE_STRING
|
c.data_type = TYPE_STRING
|
||||||
return c
|
return c
|
||||||
|
|||||||
@@ -180,7 +180,11 @@ def create_thumbnail(source, size, formats=None):
|
|||||||
except:
|
except:
|
||||||
raise ThumbnailError('Could not load image')
|
raise ThumbnailError('Could not load image')
|
||||||
|
|
||||||
frames = [resize_image(frame, size) for frame in ImageSequence.Iterator(image)]
|
frames = []
|
||||||
|
durations = []
|
||||||
|
for f in ImageSequence.Iterator(image):
|
||||||
|
durations.append(f.info.get("duration", 1000))
|
||||||
|
frames.append(resize_image(f, size))
|
||||||
image_out = frames[0]
|
image_out = frames[0]
|
||||||
save_kwargs = {}
|
save_kwargs = {}
|
||||||
source_ext = os.path.splitext(source_name)[1].lower()
|
source_ext = os.path.splitext(source_name)[1].lower()
|
||||||
@@ -198,6 +202,8 @@ def create_thumbnail(source, size, formats=None):
|
|||||||
'loop': image.info.get('loop', 0),
|
'loop': image.info.get('loop', 0),
|
||||||
'save_all': True,
|
'save_all': True,
|
||||||
}
|
}
|
||||||
|
if len(frames) > 1 and 'duration' in image.info:
|
||||||
|
save_kwargs['duration'] = durations
|
||||||
else:
|
else:
|
||||||
target_ext = 'png'
|
target_ext = 'png'
|
||||||
quality = None
|
quality = None
|
||||||
|
|||||||
+2539
-2255
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -130,7 +130,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -235,97 +235,105 @@ msgid "Canceled"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
msgid "Redeemed"
|
msgid "Confirmed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
msgid "Cancel"
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
|
msgid "Redeemed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
msgid "Cancel"
|
||||||
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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
msgid "Additional information required"
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
|
msgid "Ticket not paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "Valid ticket"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Exit recorded"
|
msgid "Additional information required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Ticket already used"
|
msgid "Valid ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Information required"
|
msgid "Exit recorded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Unknown ticket"
|
msgid "Ticket already used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
|
msgid "Unknown ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
msgid "Entry not allowed"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
|
||||||
msgid "Ticket code revoked/changed"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Ticket blocked"
|
msgid "Entry not allowed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
msgid "Order canceled"
|
msgid "Ticket blocked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order not approved"
|
msgid "Order canceled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Checked-in Tickets"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Valid Tickets"
|
msgid "Order not approved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
msgid "Currently inside"
|
msgid "Checked-in Tickets"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
|
msgid "Valid Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
|
msgid "Currently inside"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -627,23 +635,23 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
+2629
-2283
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
|
||||||
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
|
||||||
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -134,7 +134,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr "المتابعة"
|
msgstr "المتابعة"
|
||||||
|
|
||||||
@@ -239,105 +239,113 @@ msgid "Canceled"
|
|||||||
msgstr "ملغاة"
|
msgstr "ملغاة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Confirmed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
msgstr "مستخدم"
|
msgstr "مستخدم"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "قم بالإلغاء"
|
msgstr "قم بالإلغاء"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
msgid "Ticket not paid"
|
msgid "Ticket not paid"
|
||||||
msgstr "لم يتم دفع قيمة التذكرة"
|
msgstr "لم يتم دفع قيمة التذكرة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr "لم يتم دفع قيمة التذكرة بعد، هل تريد المتابعة على أي حال؟"
|
msgstr "لم يتم دفع قيمة التذكرة بعد، هل تريد المتابعة على أي حال؟"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Additional information required"
|
msgid "Additional information required"
|
||||||
msgstr "مطلوب معلومات إضافية"
|
msgstr "مطلوب معلومات إضافية"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Valid ticket"
|
msgid "Valid ticket"
|
||||||
msgstr "تذكرة سارية المفعول"
|
msgstr "تذكرة سارية المفعول"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Exit recorded"
|
msgid "Exit recorded"
|
||||||
msgstr "تم تسجيل الخروج"
|
msgstr "تم تسجيل الخروج"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Ticket already used"
|
msgid "Ticket already used"
|
||||||
msgstr "تم استخدام التذكرة مسبقا"
|
msgstr "تم استخدام التذكرة مسبقا"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Information required"
|
msgid "Information required"
|
||||||
msgstr "معلومات مطلوبة"
|
msgstr "معلومات مطلوبة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Unknown error."
|
#| msgid "Unknown error."
|
||||||
msgid "Unknown ticket"
|
msgid "Unknown ticket"
|
||||||
msgstr "خطأ غير معروف."
|
msgstr "خطأ غير معروف."
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Entry not allowed"
|
#| msgid "Entry not allowed"
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr "إدخال غير مسموح"
|
msgstr "إدخال غير مسموح"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Entry not allowed"
|
msgid "Entry not allowed"
|
||||||
msgstr "إدخال غير مسموح"
|
msgstr "إدخال غير مسموح"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket code revoked/changed"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr "تم إلغاء رمز التذكرة أو تبديله"
|
msgstr "تم إلغاء رمز التذكرة أو تبديله"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Ticket not paid"
|
#| msgid "Ticket not paid"
|
||||||
msgid "Ticket blocked"
|
msgid "Ticket blocked"
|
||||||
msgstr "لم يتم دفع قيمة التذكرة"
|
msgstr "لم يتم دفع قيمة التذكرة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Ticket not paid"
|
#| msgid "Ticket not paid"
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr "لم يتم دفع قيمة التذكرة"
|
msgstr "لم يتم دفع قيمة التذكرة"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order canceled"
|
msgid "Order canceled"
|
||||||
msgstr "تم إلغاء الطلب"
|
msgstr "تم إلغاء الطلب"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Order not approved"
|
msgid "Order not approved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
msgid "Checked-in Tickets"
|
msgid "Checked-in Tickets"
|
||||||
msgstr "تذاكر الدخول"
|
msgstr "تذاكر الدخول"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
msgid "Valid Tickets"
|
msgid "Valid Tickets"
|
||||||
msgstr "تذاكر سارية المفعول"
|
msgstr "تذاكر سارية المفعول"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
msgid "Currently inside"
|
msgid "Currently inside"
|
||||||
msgstr "حاليا بالداخل"
|
msgstr "حاليا بالداخل"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "نعم"
|
msgstr "نعم"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -657,23 +665,23 @@ msgstr "لا شيء"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "المختارة فقط"
|
msgstr "المختارة فقط"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "قم باستخدم اسم مختلف داخليا"
|
msgstr "قم باستخدم اسم مختلف داخليا"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "اضغط لاغلاق الصفحة"
|
msgstr "اضغط لاغلاق الصفحة"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "لم تقم بحفظ التعديلات!"
|
msgstr "لم تقم بحفظ التعديلات!"
|
||||||
|
|
||||||
|
|||||||
+2607
-2258
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
"PO-Revision-Date: 2020-12-19 07:00+0000\n"
|
||||||
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
"Last-Translator: albert <albert.serra.monner@gmail.com>\n"
|
||||||
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||||
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -236,97 +236,105 @@ msgid "Canceled"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
msgid "Redeemed"
|
msgid "Confirmed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
msgid "Cancel"
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
|
msgid "Redeemed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
msgid "Cancel"
|
||||||
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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
msgid "Additional information required"
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
|
msgid "Ticket not paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "Valid ticket"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Exit recorded"
|
msgid "Additional information required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Ticket already used"
|
msgid "Valid ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Information required"
|
msgid "Exit recorded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Unknown ticket"
|
msgid "Ticket already used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
|
msgid "Unknown ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
msgid "Entry not allowed"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
|
||||||
msgid "Ticket code revoked/changed"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Ticket blocked"
|
msgid "Entry not allowed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
msgid "Order canceled"
|
msgid "Ticket blocked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order not approved"
|
msgid "Order canceled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Checked-in Tickets"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Valid Tickets"
|
msgid "Order not approved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
msgid "Currently inside"
|
msgid "Checked-in Tickets"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
|
msgid "Valid Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
|
msgid "Currently inside"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -628,23 +636,23 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
+2620
-2257
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
"PO-Revision-Date: 2023-09-15 06:00+0000\n"
|
||||||
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
"Last-Translator: Michael <michael.happl@gmx.at>\n"
|
||||||
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr "Mercado Pago"
|
msgstr "Mercado Pago"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr "Pokračovat"
|
msgstr "Pokračovat"
|
||||||
|
|
||||||
@@ -236,97 +236,105 @@ msgid "Canceled"
|
|||||||
msgstr "Zrušeno"
|
msgstr "Zrušeno"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Confirmed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
msgstr "Uplatněno"
|
msgstr "Uplatněno"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Zrušit"
|
msgstr "Zrušit"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
msgid "Ticket not paid"
|
msgid "Ticket not paid"
|
||||||
msgstr "Vstupenka není zaplacena"
|
msgstr "Vstupenka není zaplacena"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr "Vstupenka nebyla zaplacena. Chcete i přesto pokračovat?"
|
msgstr "Vstupenka nebyla zaplacena. Chcete i přesto pokračovat?"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Additional information required"
|
msgid "Additional information required"
|
||||||
msgstr "Potřebné další informace"
|
msgstr "Potřebné další informace"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Valid ticket"
|
msgid "Valid ticket"
|
||||||
msgstr "Platná vstupenka"
|
msgstr "Platná vstupenka"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Exit recorded"
|
msgid "Exit recorded"
|
||||||
msgstr "Opustit nahrávané"
|
msgstr "Opustit nahrávané"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Ticket already used"
|
msgid "Ticket already used"
|
||||||
msgstr "Vstupenka již byla použita"
|
msgstr "Vstupenka již byla použita"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Information required"
|
msgid "Information required"
|
||||||
msgstr "Informace vyžadována"
|
msgstr "Informace vyžadována"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
msgid "Unknown ticket"
|
msgid "Unknown ticket"
|
||||||
msgstr "Neznámá vstupenka"
|
msgstr "Neznámá vstupenka"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr "Typ vstupenky zde není povolen"
|
msgstr "Typ vstupenky zde není povolen"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Entry not allowed"
|
msgid "Entry not allowed"
|
||||||
msgstr "Vstup není povolen"
|
msgstr "Vstup není povolen"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket code revoked/changed"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr "Kód vstupenky změněn"
|
msgstr "Kód vstupenky změněn"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
msgid "Ticket blocked"
|
msgid "Ticket blocked"
|
||||||
msgstr "Vstupenka zablokována"
|
msgstr "Vstupenka zablokována"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr "Vstupenka je v tuto chvíli neplatná"
|
msgstr "Vstupenka je v tuto chvíli neplatná"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order canceled"
|
msgid "Order canceled"
|
||||||
msgstr "Objednávka zrušena"
|
msgstr "Objednávka zrušena"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr "Kód vstupenky je v seznamu nejednoznačný"
|
msgstr "Kód vstupenky je v seznamu nejednoznačný"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Order not approved"
|
msgid "Order not approved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
msgid "Checked-in Tickets"
|
msgid "Checked-in Tickets"
|
||||||
msgstr "Vyřízené vstupenky"
|
msgstr "Vyřízené vstupenky"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
msgid "Valid Tickets"
|
msgid "Valid Tickets"
|
||||||
msgstr "Platné vstupenky"
|
msgstr "Platné vstupenky"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
msgid "Currently inside"
|
msgid "Currently inside"
|
||||||
msgstr "Aktuálně uvnitř"
|
msgstr "Aktuálně uvnitř"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ano"
|
msgstr "Ano"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -651,23 +659,23 @@ msgstr "Žádný"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Pouze vybrané"
|
msgstr "Pouze vybrané"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Interně používat jiný název"
|
msgstr "Interně používat jiný název"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Kliknutím zavřete"
|
msgstr "Kliknutím zavřete"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Máte neuložené změny!"
|
msgstr "Máte neuložené změny!"
|
||||||
|
|
||||||
|
|||||||
+2558
-2256
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -236,97 +236,105 @@ msgid "Canceled"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
msgid "Redeemed"
|
msgid "Confirmed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
msgid "Cancel"
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
|
msgid "Redeemed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
msgid "Cancel"
|
||||||
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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
msgid "Additional information required"
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
|
msgid "Ticket not paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "Valid ticket"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Exit recorded"
|
msgid "Additional information required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Ticket already used"
|
msgid "Valid ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Information required"
|
msgid "Exit recorded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Unknown ticket"
|
msgid "Ticket already used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Information required"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
|
msgid "Unknown ticket"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
msgid "Entry not allowed"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
|
||||||
msgid "Ticket code revoked/changed"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Ticket blocked"
|
msgid "Entry not allowed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
msgid "Order canceled"
|
msgid "Ticket blocked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order not approved"
|
msgid "Order canceled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Checked-in Tickets"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Valid Tickets"
|
msgid "Order not approved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
msgid "Currently inside"
|
msgid "Checked-in Tickets"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
|
msgid "Valid Tickets"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
|
msgid "Currently inside"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -628,23 +636,23 @@ msgstr ""
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
+2653
-2822
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,9 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: 2022-12-01 17:00+0000\n"
|
"PO-Revision-Date: 2024-05-30 17:00+0000\n"
|
||||||
"Last-Translator: Mie Frydensbjerg <mif@aarhus.dk>\n"
|
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
|
||||||
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"da/>\n"
|
"da/>\n"
|
||||||
"Language: da\n"
|
"Language: da\n"
|
||||||
@@ -16,7 +16,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 4.14.2\n"
|
"X-Generator: Weblate 5.5.5\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: 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:62
|
||||||
@@ -132,7 +132,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgctxt "widget"
|
#| msgctxt "widget"
|
||||||
#| msgid "Continue"
|
#| msgid "Continue"
|
||||||
@@ -240,102 +240,110 @@ msgid "Canceled"
|
|||||||
msgstr "Annulleret"
|
msgstr "Annulleret"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Confirmed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgctxt "widget"
|
#| msgctxt "widget"
|
||||||
#| msgid "Redeem"
|
#| msgid "Redeem"
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
msgstr "Indløs"
|
msgstr "Indløs"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
msgid "Ticket not paid"
|
msgid "Ticket not paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Additional information required"
|
msgid "Additional information required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Valid ticket"
|
msgid "Valid ticket"
|
||||||
msgstr "Gyldig billet"
|
msgstr "Gyldig billet"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Exit recorded"
|
msgid "Exit recorded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Ticket already used"
|
msgid "Ticket already used"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Information required"
|
msgid "Information required"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
msgid "Unknown ticket"
|
msgid "Unknown ticket"
|
||||||
msgstr "Ukendt billet"
|
msgstr "Ukendt billet"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr "Billettype er ikke tilladt her"
|
msgstr "Billettype er ikke tilladt her"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Entry not allowed"
|
msgid "Entry not allowed"
|
||||||
msgstr "Adgang ikke tilladt"
|
msgstr "Adgang ikke tilladt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket code revoked/changed"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
msgid "Ticket blocked"
|
msgid "Ticket blocked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order canceled"
|
msgid "Order canceled"
|
||||||
msgstr "Bestilling annulleret"
|
msgstr "Bestilling annulleret"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Order not approved"
|
msgid "Order not approved"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
#| msgid "Check-in QR"
|
#| msgid "Check-in QR"
|
||||||
msgid "Checked-in Tickets"
|
msgid "Checked-in Tickets"
|
||||||
msgstr "Check-in QR"
|
msgstr "Check-in QR"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
msgid "Valid Tickets"
|
msgid "Valid Tickets"
|
||||||
msgstr "Gyldige billetter"
|
msgstr "Gyldige billetter"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
msgid "Currently inside"
|
msgid "Currently inside"
|
||||||
msgstr "Inde i øjeblikket"
|
msgstr "Inde i øjeblikket"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -671,23 +679,23 @@ msgstr "Ingen"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Klik for at lukke"
|
msgstr "Klik for at lukke"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Du har ændringer, der ikke er gemt!"
|
msgstr "Du har ændringer, der ikke er gemt!"
|
||||||
|
|
||||||
@@ -724,10 +732,6 @@ msgid "Cart expired"
|
|||||||
msgstr "Kurv udløbet"
|
msgstr "Kurv udløbet"
|
||||||
|
|
||||||
#: pretix/static/pretixpresale/js/ui/cart.js:50
|
#: 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 "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_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[0] "Varerne i din kurv er reserveret for dig i et minut."
|
||||||
|
|||||||
+2605
-2319
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: 2024-03-15 18:00+0000\n"
|
"PO-Revision-Date: 2024-06-20 15:00+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||||
"de/>\n"
|
"de/>\n"
|
||||||
@@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 5.4.2\n"
|
"X-Generator: Weblate 5.5.5\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: 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:62
|
||||||
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr "Mercado Pago"
|
msgstr "Mercado Pago"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr "Fortfahren"
|
msgstr "Fortfahren"
|
||||||
|
|
||||||
@@ -236,97 +236,105 @@ msgid "Canceled"
|
|||||||
msgstr "storniert"
|
msgstr "storniert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Confirmed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
msgstr "Eingelöst"
|
msgstr "Eingelöst"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Abbrechen"
|
msgstr "Abbrechen"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
msgid "Ticket not paid"
|
msgid "Ticket not paid"
|
||||||
msgstr "Ticket nicht bezahlt"
|
msgstr "Ticket nicht bezahlt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr "Dieses Ticket ist noch nicht bezahlt. Möchten Sie dennoch fortfahren?"
|
msgstr "Dieses Ticket ist noch nicht bezahlt. Möchten Sie dennoch fortfahren?"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Additional information required"
|
msgid "Additional information required"
|
||||||
msgstr "Zusätzliche Informationen benötigt"
|
msgstr "Zusätzliche Informationen benötigt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Valid ticket"
|
msgid "Valid ticket"
|
||||||
msgstr "Gültiges Ticket"
|
msgstr "Gültiges Ticket"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Exit recorded"
|
msgid "Exit recorded"
|
||||||
msgstr "Ausgang gespeichert"
|
msgstr "Ausgang gespeichert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Ticket already used"
|
msgid "Ticket already used"
|
||||||
msgstr "Ticket bereits eingelöst"
|
msgstr "Ticket bereits eingelöst"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Information required"
|
msgid "Information required"
|
||||||
msgstr "Infos benötigt"
|
msgstr "Infos benötigt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
msgid "Unknown ticket"
|
msgid "Unknown ticket"
|
||||||
msgstr "Unbekanntes Ticket"
|
msgstr "Unbekanntes Ticket"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr "Ticketart hier nicht erlaubt"
|
msgstr "Ticketart hier nicht erlaubt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Entry not allowed"
|
msgid "Entry not allowed"
|
||||||
msgstr "Eingang nicht erlaubt"
|
msgstr "Eingang nicht erlaubt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket code revoked/changed"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr "Ticket-Code gesperrt/geändert"
|
msgstr "Ticket-Code gesperrt/geändert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
msgid "Ticket blocked"
|
msgid "Ticket blocked"
|
||||||
msgstr "Ticket gesperrt"
|
msgstr "Ticket gesperrt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr "Ticket aktuell nicht gültig"
|
msgstr "Ticket aktuell nicht gültig"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order canceled"
|
msgid "Order canceled"
|
||||||
msgstr "Bestellung storniert"
|
msgstr "Bestellung storniert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr "Ticket-Code ist nicht eindeutig auf der Liste"
|
msgstr "Ticket-Code ist nicht eindeutig auf der Liste"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Order not approved"
|
msgid "Order not approved"
|
||||||
msgstr "Bestellung nicht freigegeben"
|
msgstr "Bestellung nicht freigegeben"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
msgid "Checked-in Tickets"
|
msgid "Checked-in Tickets"
|
||||||
msgstr "Eingecheckte Tickets"
|
msgstr "Eingecheckte Tickets"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
msgid "Valid Tickets"
|
msgid "Valid Tickets"
|
||||||
msgstr "Gültige Tickets"
|
msgstr "Gültige Tickets"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
msgid "Currently inside"
|
msgid "Currently inside"
|
||||||
msgstr "Derzeit anwesend"
|
msgstr "Derzeit anwesend"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -435,7 +443,7 @@ msgstr "ist nach"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
|
||||||
msgid "="
|
msgid "="
|
||||||
msgstr ""
|
msgstr "="
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
||||||
msgid "Product"
|
msgid "Product"
|
||||||
@@ -459,7 +467,7 @@ msgstr "Aktueller Tag der Woche (1 = Montag, 7 = Sonntag)"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||||
msgid "Current entry status"
|
msgid "Current entry status"
|
||||||
msgstr ""
|
msgstr "Aktueller Zutrittsstatus"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||||
msgid "Number of previous entries"
|
msgid "Number of previous entries"
|
||||||
@@ -544,12 +552,12 @@ msgstr "Duplizieren"
|
|||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
|
||||||
msgctxt "entry_status"
|
msgctxt "entry_status"
|
||||||
msgid "present"
|
msgid "present"
|
||||||
msgstr ""
|
msgstr "anwesend"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
|
||||||
msgctxt "entry_status"
|
msgctxt "entry_status"
|
||||||
msgid "absent"
|
msgid "absent"
|
||||||
msgstr ""
|
msgstr "abwesend"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
@@ -649,23 +657,23 @@ msgstr "Keine"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Nur ausgewählte"
|
msgstr "Nur ausgewählte"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
|
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr "Ungültige Seitenzahl."
|
msgstr "Ungültige Seitenzahl."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Intern einen anderen Namen verwenden"
|
msgstr "Intern einen anderen Namen verwenden"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Klicken zum Schließen"
|
msgstr "Klicken zum Schließen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Sie haben ungespeicherte Änderungen!"
|
msgstr "Sie haben ungespeicherte Änderungen!"
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Backend
|
|||||||
Badge
|
Badge
|
||||||
Badges
|
Badges
|
||||||
Bancontact
|
Bancontact
|
||||||
|
BankID
|
||||||
Banking
|
Banking
|
||||||
barcodes
|
barcodes
|
||||||
Bcc
|
Bcc
|
||||||
@@ -301,6 +302,7 @@ Strg
|
|||||||
Stripe
|
Stripe
|
||||||
Stripes
|
Stripes
|
||||||
Strong
|
Strong
|
||||||
|
Swish
|
||||||
systemweiten
|
systemweiten
|
||||||
Tab
|
Tab
|
||||||
tag
|
tag
|
||||||
@@ -318,6 +320,7 @@ Tracking
|
|||||||
transaktionale
|
transaktionale
|
||||||
Trustly
|
Trustly
|
||||||
Turnover
|
Turnover
|
||||||
|
TWINT
|
||||||
txt
|
txt
|
||||||
überbuchen
|
überbuchen
|
||||||
überbucht
|
überbucht
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-04-23 12:41+0000\n"
|
"POT-Creation-Date: 2024-06-24 08:20+0000\n"
|
||||||
"PO-Revision-Date: 2024-03-15 18:00+0000\n"
|
"PO-Revision-Date: 2024-06-20 15:00+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||||
"pretix/pretix-js/de_Informal/>\n"
|
"pretix/pretix-js/de_Informal/>\n"
|
||||||
@@ -17,7 +17,7 @@ msgstr ""
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 5.4.2\n"
|
"X-Generator: Weblate 5.5.5\n"
|
||||||
|
|
||||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
#: 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:62
|
||||||
@@ -131,7 +131,7 @@ msgid "Mercado Pago"
|
|||||||
msgstr "Mercado Pago"
|
msgstr "Mercado Pago"
|
||||||
|
|
||||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:167
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr "Fortfahren"
|
msgstr "Fortfahren"
|
||||||
|
|
||||||
@@ -236,97 +236,105 @@ msgid "Canceled"
|
|||||||
msgstr "storniert"
|
msgstr "storniert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:46
|
||||||
|
msgid "Confirmed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
||||||
|
msgid "Approval pending"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||||
msgid "Redeemed"
|
msgid "Redeemed"
|
||||||
msgstr "Eingelöst"
|
msgstr "Eingelöst"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:47
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
msgstr "Abbrechen"
|
msgstr "Abbrechen"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
||||||
msgid "Ticket not paid"
|
msgid "Ticket not paid"
|
||||||
msgstr "Ticket nicht bezahlt"
|
msgstr "Ticket nicht bezahlt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:50
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
||||||
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
msgid "This ticket is not yet paid. Do you want to continue anyways?"
|
||||||
msgstr "Dieses Ticket ist noch nicht bezahlt. Möchtest du dennoch fortfahren?"
|
msgstr "Dieses Ticket ist noch nicht bezahlt. Möchtest du dennoch fortfahren?"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:51
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
||||||
msgid "Additional information required"
|
msgid "Additional information required"
|
||||||
msgstr "Zusätzliche Informationen benötigt"
|
msgstr "Zusätzliche Informationen benötigt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:52
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
||||||
msgid "Valid ticket"
|
msgid "Valid ticket"
|
||||||
msgstr "Gültiges Ticket"
|
msgstr "Gültiges Ticket"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:53
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
||||||
msgid "Exit recorded"
|
msgid "Exit recorded"
|
||||||
msgstr "Ausgang gespeichert"
|
msgstr "Ausgang gespeichert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:54
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
||||||
msgid "Ticket already used"
|
msgid "Ticket already used"
|
||||||
msgstr "Ticket bereits eingelöst"
|
msgstr "Ticket bereits eingelöst"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:55
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
||||||
msgid "Information required"
|
msgid "Information required"
|
||||||
msgstr "Infos benötigt"
|
msgstr "Infos benötigt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:56
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:58
|
||||||
msgid "Unknown ticket"
|
msgid "Unknown ticket"
|
||||||
msgstr "Unbekanntes Ticket"
|
msgstr "Unbekanntes Ticket"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:57
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
||||||
msgid "Ticket type not allowed here"
|
msgid "Ticket type not allowed here"
|
||||||
msgstr "Ticketart hier nicht erlaubt"
|
msgstr "Ticketart hier nicht erlaubt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:59
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
||||||
msgid "Entry not allowed"
|
msgid "Entry not allowed"
|
||||||
msgstr "Eingang nicht erlaubt"
|
msgstr "Eingang nicht erlaubt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:60
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
||||||
msgid "Ticket code revoked/changed"
|
msgid "Ticket code revoked/changed"
|
||||||
msgstr "Ticket-Code gesperrt/geändert"
|
msgstr "Ticket-Code gesperrt/geändert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:61
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||||
msgid "Ticket blocked"
|
msgid "Ticket blocked"
|
||||||
msgstr "Ticket gesperrt"
|
msgstr "Ticket gesperrt"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:62
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||||
msgid "Ticket not valid at this time"
|
msgid "Ticket not valid at this time"
|
||||||
msgstr "Ticket aktuell nicht gültig"
|
msgstr "Ticket aktuell nicht gültig"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
||||||
msgid "Order canceled"
|
msgid "Order canceled"
|
||||||
msgstr "Bestellung storniert"
|
msgstr "Bestellung storniert"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
||||||
msgid "Ticket code is ambiguous on list"
|
msgid "Ticket code is ambiguous on list"
|
||||||
msgstr "Ticket-Code ist nicht eindeutig auf der Liste"
|
msgstr "Ticket-Code ist nicht eindeutig auf der Liste"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:65
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
||||||
msgid "Order not approved"
|
msgid "Order not approved"
|
||||||
msgstr "Bestellung nicht freigegeben"
|
msgstr "Bestellung nicht freigegeben"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:66
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||||
msgid "Checked-in Tickets"
|
msgid "Checked-in Tickets"
|
||||||
msgstr "Eingecheckte Tickets"
|
msgstr "Eingecheckte Tickets"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:67
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||||
msgid "Valid Tickets"
|
msgid "Valid Tickets"
|
||||||
msgstr "Gültige Tickets"
|
msgstr "Gültige Tickets"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
||||||
msgid "Currently inside"
|
msgid "Currently inside"
|
||||||
msgstr "Derzeit anwesend"
|
msgstr "Derzeit anwesend"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:71
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
#: pretix/static/pretixcontrol/js/ui/question.js:137
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "Yes"
|
msgid "Yes"
|
||||||
msgstr "Ja"
|
msgstr "Ja"
|
||||||
|
|
||||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:70
|
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:72
|
||||||
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
#: pretix/static/pretixcontrol/js/ui/question.js:138
|
||||||
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
#: pretix/static/pretixpresale/js/ui/questions.js:270
|
||||||
msgid "No"
|
msgid "No"
|
||||||
@@ -434,7 +442,7 @@ msgstr "ist nach"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:40
|
||||||
msgid "="
|
msgid "="
|
||||||
msgstr ""
|
msgstr "="
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
||||||
msgid "Product"
|
msgid "Product"
|
||||||
@@ -458,7 +466,7 @@ msgstr "Aktueller Wochentag (1 = Montag, 7 = Sonntag)"
|
|||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:119
|
||||||
msgid "Current entry status"
|
msgid "Current entry status"
|
||||||
msgstr ""
|
msgstr "Aktueller Zutrittsstatus"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:123
|
||||||
msgid "Number of previous entries"
|
msgid "Number of previous entries"
|
||||||
@@ -543,12 +551,12 @@ msgstr "Duplizieren"
|
|||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:193
|
||||||
msgctxt "entry_status"
|
msgctxt "entry_status"
|
||||||
msgid "present"
|
msgid "present"
|
||||||
msgstr ""
|
msgstr "anwesend"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
|
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:194
|
||||||
msgctxt "entry_status"
|
msgctxt "entry_status"
|
||||||
msgid "absent"
|
msgid "absent"
|
||||||
msgstr ""
|
msgstr "abwesend"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
#: pretix/static/pretixcontrol/js/ui/editor.js:72
|
||||||
msgid "Check-in QR"
|
msgid "Check-in QR"
|
||||||
@@ -648,23 +656,23 @@ msgstr "Keine"
|
|||||||
msgid "Selected only"
|
msgid "Selected only"
|
||||||
msgstr "Nur ausgewählte"
|
msgstr "Nur ausgewählte"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:794
|
#: pretix/static/pretixcontrol/js/ui/main.js:828
|
||||||
msgid "Enter page number between 1 and %(max)s."
|
msgid "Enter page number between 1 and %(max)s."
|
||||||
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
|
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:797
|
#: pretix/static/pretixcontrol/js/ui/main.js:831
|
||||||
msgid "Invalid page number."
|
msgid "Invalid page number."
|
||||||
msgstr "Ungültige Seitenzahl."
|
msgstr "Ungültige Seitenzahl."
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:955
|
#: pretix/static/pretixcontrol/js/ui/main.js:989
|
||||||
msgid "Use a different name internally"
|
msgid "Use a different name internally"
|
||||||
msgstr "Intern einen anderen Namen verwenden"
|
msgstr "Intern einen anderen Namen verwenden"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:995
|
#: pretix/static/pretixcontrol/js/ui/main.js:1029
|
||||||
msgid "Click to close"
|
msgid "Click to close"
|
||||||
msgstr "Klicken zum Schließen"
|
msgstr "Klicken zum Schließen"
|
||||||
|
|
||||||
#: pretix/static/pretixcontrol/js/ui/main.js:1070
|
#: pretix/static/pretixcontrol/js/ui/main.js:1104
|
||||||
msgid "You have unsaved changes!"
|
msgid "You have unsaved changes!"
|
||||||
msgstr "Du hast ungespeicherte Änderungen!"
|
msgstr "Du hast ungespeicherte Änderungen!"
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Backend
|
|||||||
Badge
|
Badge
|
||||||
Badges
|
Badges
|
||||||
Bancontact
|
Bancontact
|
||||||
|
BankID
|
||||||
Banking
|
Banking
|
||||||
barcodes
|
barcodes
|
||||||
Bcc
|
Bcc
|
||||||
@@ -301,6 +302,7 @@ Strg
|
|||||||
Stripe
|
Stripe
|
||||||
Stripes
|
Stripes
|
||||||
Strong
|
Strong
|
||||||
|
Swish
|
||||||
systemweiten
|
systemweiten
|
||||||
Tab
|
Tab
|
||||||
tag
|
tag
|
||||||
@@ -318,6 +320,7 @@ Tracking
|
|||||||
transaktionale
|
transaktionale
|
||||||
Trustly
|
Trustly
|
||||||
Turnover
|
Turnover
|
||||||
|
TWINT
|
||||||
txt
|
txt
|
||||||
überbuchen
|
überbuchen
|
||||||
überbucht
|
überbucht
|
||||||
|
|||||||
+2539
-2255
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
Reference in New Issue
Block a user