Compare commits

..
Author SHA1 Message Date
Lukas Bockstaller 48356ea19e code style 2026-08-25 11:09:26 +02:00
Lukas Bockstaller 6fa91b37ab validate that the sale has any captures before marking paid 2026-08-25 11:07:15 +02:00
187 changed files with 13601 additions and 14012 deletions
-2
View File
@@ -1,2 +0,0 @@
# Format pre-vue code with eslint where possible (2026-09-10)
d85e52c83ed3639e040372fd0052e842e4899d90
-43
View File
@@ -1,43 +0,0 @@
name: SBOM
on:
push:
branches: [ master, sbom ]
tags: [ 'v.*' ]
permissions:
contents: read # to fetch code (actions/checkout)
env:
FORCE_COLOR: 1
jobs:
test:
runs-on: ubuntu-22.04
name: Submission
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Use Node.js
uses: actions/setup-node@v7
with:
node-version: '24.x'
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install system dependencies
run: sudo apt update && sudo apt install -y gettext unzip
- name: Install Python dependencies
run: pip3 install -U "prisma-sbom-submit[python]"
- name: Create SBOM
run: NPM=$(which npm) prisma-sbom-submit collect . sbom.json
- name: Submit SBOM
run: prisma-sbom-submit upload --server https://prisma.pretix.com sbom.json
env:
PRISMA_UPLOAD_TOKEN: ${{ secrets.PRISMA_UPLOAD_TOKEN }}
+2 -19
View File
@@ -123,24 +123,7 @@ jobs:
working-directory: ./src working-directory: ./src
run: make all compress run: make all compress
- name: Install Playwright browsers - name: Install Playwright browsers
run: playwright install --with-deps run: playwright install
- name: Run E2E tests - name: Run E2E tests
working-directory: ./src working-directory: ./src
run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10 --tracing=retain-on-failure run: PRETIX_CONFIG_FILE=tests/ci_postgres.cfg py.test tests/e2e/ -v --maxfail=10
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-traces
path: test-results/
- name: Log trace instructions
if: steps.check-traces.outputs.found == 'true'
run: |
{
echo "## 🎭 Playwright traces available"
echo ""
echo "Some tests failed or retried and produced traces."
echo ""
echo "1. Download the **playwright-traces-${{ github.run_id }}** artifact from this run (link in the **Summary** tab, under Artifacts)."
echo "2. Unzip it."
echo "3. Go to https://trace.playwright.dev and drag \`trace.zip\` into the page — or run \`npx playwright show-trace trace.zip\` locally."
} >> "$GITHUB_STEP_SUMMARY"
+2
View File
@@ -16,6 +16,8 @@ recursive-include src/pretix/plugins/banktransfer/templates *
recursive-include src/pretix/plugins/banktransfer/static * recursive-include src/pretix/plugins/banktransfer/static *
recursive-include src/pretix/plugins/manualpayment/templates * recursive-include src/pretix/plugins/manualpayment/templates *
recursive-include src/pretix/plugins/manualpayment/static * recursive-include src/pretix/plugins/manualpayment/static *
recursive-include src/pretix/plugins/paypal/templates *
recursive-include src/pretix/plugins/paypal/static *
recursive-include src/pretix/plugins/paypal2/templates * recursive-include src/pretix/plugins/paypal2/templates *
recursive-include src/pretix/plugins/paypal2/static * recursive-include src/pretix/plugins/paypal2/static *
recursive-include src/pretix/plugins/src/pretixdroid/templates * recursive-include src/pretix/plugins/src/pretixdroid/templates *
-241
View File
@@ -1,241 +0,0 @@
Event Meta Properties
=====================
Resource description
--------------------
An event meta property is used to to define meta information fields for its events.
This information can be re-used, for example, in ticket layouts.
The event meta property resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Unique ID for this property
name string Name of the property
default string Value of the default option
required boolean If ``true``, an event can only be taken live if the
property is set. In event series, it's always optional
to set a value for individual dates
protected boolean If ``true``, the value for an event can only be changed
by organizer-level administrators
filter_public boolean If ``true``, this property will be shown to filter
events in the public event list and calendar
public_label string Public name of the property
filter_allowed boolean If ``true``, this property will be shown to filter
events or reports in the backend, and it can also be
used for hidden filter parameters in the frontend
choices list of objects List of JSON objects representing all permitted values
for this property, or ``null`` for no limitation.
Each choice object has a required internal name named
``key`` and optional public name named ``label``
consisting of a dictionary of i18n string translations
===================================== ========================== =======================================================
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/
Returns a list of all meta properties for the organizer.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": [
{
"key": "blue",
"label": {
"en": "Blue"
},
}
]
}
]
}
:param organizer: The ``slug`` field of the organizer
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Returns information on one property, identified by its id.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
{
"id": 1,
"name": "Color",
"default": "blue",
"required": false,
"protected": false,
"filter_public": false,
"public_label": {},
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to retrieve
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/event_meta_properties/
Creates a new meta property
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/event_meta_properties/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"name": "ref-code",
"default": "abcde",
"required": true,
"choices": null
}
**Example response**:
.. sourcecode:: http
{
"id": 2,
"name": "reference",
"default": "abcde",
"required": true,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:statuscode 201: no error
:statuscode 400: The meta property could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Update a meta property. You can also use ``PUT`` instead of ``PATCH``. With ``PUT``, you have to provide
all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the
fields that you want to change.
You can change all fields of the resource except the ``id`` field.
**Example request**:
.. sourcecode:: http
PATCH /api/v1/organizers/bigevents/event_meta_properties/2/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"required": false
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 3,
"name": "reference",
"default": "abcde",
"required": false,
"protected": false,
"filter_public": false,
"public_label": null,
"filter_allowed": true,
"choices": null
}
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to modify
:statuscode 200: no error
:statuscode 400: The property could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/event_meta_properties/(id)/
Delete a meta property.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/event_meta_properties/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 204 No Content
Vary: Accept
:param organizer: The ``slug`` field of the organizer
:param id: The ``id`` field of the meta property to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer does not exist **or** you have no permission to delete this resource.
+15 -17
View File
@@ -110,7 +110,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2", "pretix.plugins.paypal",
"pretix.plugins.ticketoutputpdf" "pretix.plugins.ticketoutputpdf"
], ],
"all_sales_channels": false, "all_sales_channels": false,
@@ -199,7 +199,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2", "pretix.plugins.paypal",
"pretix.plugins.ticketoutputpdf" "pretix.plugins.ticketoutputpdf"
], ],
"valid_keys": { "valid_keys": {
@@ -262,7 +262,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2" "pretix.plugins.paypal"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [] "limit_sales_channels": []
@@ -299,7 +299,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2" "pretix.plugins.paypal"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [], "limit_sales_channels": [],
@@ -364,7 +364,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2" "pretix.plugins.paypal"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [] "limit_sales_channels": []
@@ -401,7 +401,7 @@ Endpoints
"item_meta_properties": {}, "item_meta_properties": {},
"plugins": [ "plugins": [
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2" "pretix.plugins.paypal"
], ],
"all_sales_channels": true, "all_sales_channels": true,
"limit_sales_channels": [], "limit_sales_channels": [],
@@ -438,7 +438,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2", "pretix.plugins.paypal",
"pretix.plugins.pretixdroid" "pretix.plugins.pretixdroid"
] ]
} }
@@ -475,7 +475,7 @@ Endpoints
"plugins": [ "plugins": [
"pretix.plugins.banktransfer", "pretix.plugins.banktransfer",
"pretix.plugins.stripe", "pretix.plugins.stripe",
"pretix.plugins.paypal2", "pretix.plugins.paypal",
"pretix.plugins.pretixdroid" "pretix.plugins.pretixdroid"
], ],
"all_sales_channels": true, "all_sales_channels": true,
@@ -566,7 +566,7 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"region": "DE", "imprint_url": "https://pretix.eu",
… …
} }
@@ -579,14 +579,12 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"region": "imprint_url":
{ {
"value": "DE", "value": "https://pretix.eu",
"label": "Region", "label": "Imprint URL",
"readonly": false, "readonly": false,
"help_text": "Will be used to determine date and time formatting as well as default country for customer "help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
addresses and phone numbers. For formatting, this takes less priority than the language and
is therefore mostly relevant for languages used in different regions globally (like English)."
} }
}, },
… …
@@ -622,7 +620,7 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"region": "DE" "imprint_url": "https://example.org/imprint/"
} }
**Example response**: **Example response**:
@@ -634,7 +632,7 @@ organizer level.
Content-Type: application/json Content-Type: application/json
{ {
"region": "DE", "imprint_url": "https://example.org/imprint/",
… …
} }
-1
View File
@@ -12,7 +12,6 @@ at :ref:`plugin-docs`.
organizers organizers
events events
subevents subevents
event_meta_properties
taxrules taxrules
categories categories
items items
-1
View File
@@ -116,7 +116,6 @@ Endpoints
:query integer page: The page number in case of a multi-page result set, default is 1 :query integer page: The page number in case of a multi-page result set, default is 1
:query string code: Only show the voucher with the given voucher code. :query string code: Only show the voucher with the given voucher code.
:query string search: Only show the voucher with the given query found in the code, tag, or comment.
:query integer max_usages: Only show vouchers with the given maximal number of usages. :query integer max_usages: Only show vouchers with the given maximal number of usages.
:query integer redeemed: Only show vouchers with the given number of redemptions. Note that this doesn't tell you if :query integer redeemed: Only show vouchers with the given number of redemptions. Note that this doesn't tell you if
the voucher can still be redeemed, as this also depends on ``max_usages``. See the the voucher can still be redeemed, as this also depends on ``max_usages``. See the
+1 -58
View File
@@ -8,64 +8,7 @@ import vuePug from 'eslint-plugin-vue-pug'
const ignores = globalIgnores([ const ignores = globalIgnores([
'**/node_modules', '**/node_modules',
'**/dist', '**/dist'
// Vendored code
'src/pretix/static/leaflet',
'src/pretix/static/clipboard',
'src/pretix/static/cropper',
'src/pretix/static/lightbox',
'src/pretix/static/are-you-sure',
'src/pretix/static/vuejs',
'src/pretix/static/fontawesome',
'src/pretix/static/typeahead',
'src/pretix/static/moment',
'src/pretix/static/pdfjs',
'src/pretix/static/sortable',
'src/pretix/static/iframeresizer',
'src/pretix/static/bootstrap',
'src/pretix/static/d3',
'src/pretix/static/jsi18n',
'src/pretix/static/fabric',
'src/pretix/static/datetimepicker',
'src/pretix/static/charts',
'src/pretix/static/fileupload',
'src/pretix/static/seating',
'src/pretix/static/rest_framework',
'src/pretix/static/select2',
'src/pretix/static/schema',
'src/pretix/static/slider',
'src/pretix/static/jquery',
'src/pretix/static/colorpicker',
'src/pretix/static/rrule',
'src/pretix/static/pretixcontrol/js/jquery.qrcode.min.js',
'src/pretix/static/pretixpresale/js/widget/docready.js',
// Pre-vue JS code
'src/pretix/static/pretixbase/js/addressform.js',
'src/pretix/static/pretixbase/js/asynctask.js',
'src/pretix/static/pretixbase/js/details.js',
'src/pretix/static/pretixbase/js/gettextstub.js',
'src/pretix/static/pretixbase/js/i18nstring.js',
'src/pretix/static/pretixcontrol/js/menu.js',
'src/pretix/static/pretixcontrol/js/ui/editor.js',
'src/pretix/static/pretixcontrol/js/ui/geo.js',
'src/pretix/static/pretixcontrol/js/ui/main.js',
'src/pretix/static/pretixcontrol/js/ui/plugins.js',
'src/pretix/static/pretixcontrol/js/ui/subevent.js',
'src/pretix/static/pretixcontrol/js/ui/variations.js',
'src/pretix/static/pretixcontrol/js/ui/webauthn.js',
'src/pretix/static/pretixpresale/js/ui/cart.js',
'src/pretix/static/pretixpresale/js/ui/main.js',
'src/pretix/static/pretixpresale/js/ui/questions.js',
'src/pretix/static/pretixpresale/js/widget/floatformat.js',
'src/pretix/static/pretixpresale/js/widget/widget.js',
'src/pretix/plugins/banktransfer/static',
'src/pretix/plugins/paypal2/static',
'src/pretix/plugins/statistics/static',
'src/pretix/plugins/stripe/static',
// Plugin checkouts
'local',
// docs
'doc',
]) ])
export default defineConfig([ export default defineConfig([
+13 -27
View File
@@ -294,43 +294,29 @@
} }
}, },
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/node": { "node_modules/@humanfs/node": {
"version": "0.16.8", "version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@humanfs/core": "^0.19.2", "@humanfs/core": "^0.19.1",
"@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0" "@humanwhocodes/retry": "^0.4.0"
}, },
"engines": { "engines": {
"node": ">=18.18.0" "node": ">=18.18.0"
} }
}, },
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": { "node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -3394,9 +3380,9 @@
} }
}, },
"node_modules/postcss-selector-parser": { "node_modules/postcss-selector-parser": {
"version": "7.1.5", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
"integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -4162,9 +4148,9 @@
} }
}, },
"node_modules/smol-toml": { "node_modules/smol-toml": {
"version": "1.7.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==", "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
+10 -10
View File
@@ -33,14 +33,14 @@ dependencies = [
"bleach==6.4.*", "bleach==6.4.*",
"celery==5.6.*", "celery==5.6.*",
"chardet==5.2.*", "chardet==5.2.*",
"cryptography>=50.0.1", "cryptography>=50.0.0",
"css-inline==0.21.*", "css-inline==0.21.*",
"defusedcsv>=3.0.0", "defusedcsv>=3.0.0",
"dnspython==2.*", "dnspython==2.*",
"Django[argon2]==5.2.*,>=5.2.17", "Django[argon2]==5.2.*",
"django-bootstrap3==26.2", "django-bootstrap3==26.2",
"django-compressor==4.6.0", "django-compressor==4.6.0",
"django-countries==9.1.*", "django-countries==9.0.*",
"django-filter==26.1", "django-filter==26.1",
"django-formset-js-improved==0.5.0.5", "django-formset-js-improved==0.5.0.5",
"django-formtools==2.7", "django-formtools==2.7",
@@ -56,7 +56,7 @@ dependencies = [
"django-querytagger==0.0.3", "django-querytagger==0.0.3",
"django-redis==7.0.*", "django-redis==7.0.*",
"django-scopes==2.1.*", "django-scopes==2.1.*",
"django-statici18n==2.8.*", "django-statici18n==2.7.*",
"djangorestframework==3.17.*", "djangorestframework==3.17.*",
"dnspython==2.8.*", "dnspython==2.8.*",
"drf_ujson2==1.7.*", "drf_ujson2==1.7.*",
@@ -75,16 +75,16 @@ dependencies = [
"packaging", "packaging",
"paypalrestsdk==1.13.*", "paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*", "paypal-checkout-serversdk==1.0.*",
"PyJWT==2.14.*", "PyJWT==2.13.*",
"phonenumberslite==9.0.*", "phonenumberslite==9.0.*",
"Pillow==12.3.*", "Pillow==12.3.*",
"pretix-plugin-build", "pretix-plugin-build",
"protobuf==7.36.*", "protobuf==7.35.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
"pycparser==3.0", "pycparser==3.0",
"pycryptodome==3.23.*", "pycryptodome==3.23.*",
"pypdf==6.19.*", "pypdf==6.5.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab "python-bidi==0.6.*", # Support for Arabic in reportlab
"python-dateutil==2.9.*", "python-dateutil==2.9.*",
"pytz", "pytz",
@@ -94,7 +94,7 @@ dependencies = [
"redis==7.4.*", "redis==7.4.*",
"reportlab==5.0.*", "reportlab==5.0.*",
"requests==2.34.*", "requests==2.34.*",
"sentry-sdk==2.69.*", "sentry-sdk==2.68.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",
@@ -112,10 +112,10 @@ dev = [
"aiohttp==3.14.*", "aiohttp==3.14.*",
"coverage", "coverage",
"coveralls", "coveralls",
"fakeredis==2.38.*", "fakeredis==2.37.*",
"flake8==7.3.*", "flake8==7.3.*",
"freezegun", "freezegun",
"isort==9.0.*", "isort==8.0.*",
"pep8-naming==0.15.*", "pep8-naming==0.15.*",
"potypo", "potypo",
"pytest-asyncio>=1.4.0", "pytest-asyncio>=1.4.0",
+1
View File
@@ -26,6 +26,7 @@ ignore =
src/tests/plugins/* src/tests/plugins/*
src/tests/plugins/badges/* src/tests/plugins/badges/*
src/tests/plugins/banktransfer/* src/tests/plugins/banktransfer/*
src/tests/plugins/paypal/*
src/tests/plugins/paypal2/* src/tests/plugins/paypal2/*
src/tests/plugins/pretixdroid/* src/tests/plugins/pretixdroid/*
src/tests/plugins/stripe/* src/tests/plugins/stripe/*
+3 -90
View File
@@ -28,7 +28,6 @@ from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.translation import gettext, gettext_lazy as _ from django.utils.translation import gettext, gettext_lazy as _
from i18nfield.rest_framework import I18nField
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -41,10 +40,9 @@ from pretix.api.serializers.settings import SettingsSerializer
from pretix.base.auth import get_auth_backends from pretix.base.auth import get_auth_backends
from pretix.base.i18n import get_language_without_region from pretix.base.i18n import get_language_without_region
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, EventMetaProperty, GiftCard, GiftCardAcceptance, Customer, Device, GiftCard, GiftCardAcceptance, GiftCardTransaction,
GiftCardTransaction, Membership, MembershipType, OrderPosition, Organizer, Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
ReusableMedium, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
User,
) )
from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.permissions import ( from pretix.base.permissions import (
@@ -642,88 +640,3 @@ class OrganizerSettingsSerializer(SettingsSerializer):
) )
# TODO: make sure pub is always correct # TODO: make sure pub is always correct
return 'pub/' + fname return 'pub/' + fname
class MetaPropertyListField(serializers.ListField):
def __init__(self, *args, **kwargs):
kwargs["validators"] = kwargs.pop("validators", [])
def validate_keys_unique(choices):
if not choices:
return
keys = [c.get("key") for c in choices]
if len(set(keys)) < len(keys):
raise ValidationError("The key for each meta property value option must be unique.")
kwargs["validators"].append(
validate_keys_unique
)
super().__init__(*args, **kwargs)
class MetaPropertyDictField(serializers.DictField):
def __init__(self, **kwargs):
self.label_child = kwargs.pop("label_child", I18nField())
super().__init__(**kwargs)
def to_representation(self, value):
# django added unneccessary keys DELETE, ORDER through formsets, filter them here for backwards compat
d = {
"key": value["key"]
}
if "label" in value:
d["label"] = self.label_child.to_representation(value["label"])
return super().to_representation(d)
def to_internal_value(self, data):
if not isinstance(data, dict):
raise ValidationError("Meta property value options must be a dict.")
if not isinstance(data.get("key"), str):
raise ValidationError("Meta property value options must have a key of type string.")
if any(k not in {"key", "label"} for k in data.keys()):
raise ValidationError("Meta property value options may only have a key and optionally a label.")
if "label" in data:
try:
data["label"] = self.label_child.to_internal_value(data["label"])
except ValidationError as e:
raise ValidationError({"label": e.detail})
return super().to_internal_value(data)
class EventMetaPropertiesSerializer(I18nAwareModelSerializer):
choices = MetaPropertyListField(
child=MetaPropertyDictField(
label_child=I18nField()
),
allow_null=True,
)
class Meta:
model = EventMetaProperty
fields = (
'id', 'name', 'default', 'required', 'protected', 'filter_public', 'public_label', 'filter_allowed',
'choices'
)
def validate(self, data):
data = super().validate(data)
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
choices = full_data.get("choices")
default = full_data.get("default")
if choices and default:
choice_keys = [c.get("key") for c in choices]
if default not in choice_keys:
raise ValidationError("You cannot set a default value that is not a valid value.")
if not choices and "choices" in data:
# normalize empty dict to None
data["choices"] = None
return data
-1
View File
@@ -68,7 +68,6 @@ orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportVie
orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters') orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters')
orga_router.register(r'transactions', order.OrganizerTransactionViewSet) orga_router.register(r'transactions', order.OrganizerTransactionViewSet)
orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions') orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions')
orga_router.register(r'event_meta_properties', organizer.EventMetaPropertiesViewSet)
team_router = routers.DefaultRouter() team_router = routers.DefaultRouter()
team_router.register(r'members', organizer.TeamMemberViewSet) team_router.register(r'members', organizer.TeamMemberViewSet)
+4 -51
View File
@@ -44,16 +44,15 @@ from pretix.api.models import OAuthAccessToken
from pretix.api.pagination import TotalOrderingFilter from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.organizer import ( from pretix.api.serializers.organizer import (
CustomerCreateSerializer, CustomerSerializer, DeviceSerializer, CustomerCreateSerializer, CustomerSerializer, DeviceSerializer,
EventMetaPropertiesSerializer, GiftCardSerializer, GiftCardSerializer, GiftCardTransactionSerializer, MembershipSerializer,
GiftCardTransactionSerializer, MembershipSerializer,
MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer, MembershipTypeSerializer, OrganizerSerializer, OrganizerSettingsSerializer,
SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer, SalesChannelSerializer, SeatingPlanSerializer, TeamAPITokenSerializer,
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer, TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
) )
from pretix.base.models import ( from pretix.base.models import (
Customer, Device, Event, EventMetaProperty, GiftCard, GiftCardTransaction, Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
LogEntry, Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
Team, TeamAPIToken, TeamInvite, User, TeamAPIToken, TeamInvite, User,
) )
from pretix.base.plugins import ( from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
@@ -847,49 +846,3 @@ class SalesChannelViewSet(viewsets.ModelViewSet):
data={'id': instance.pk} data={'id': instance.pk}
) )
instance.delete() instance.delete()
class EventMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = EventMetaPropertiesSerializer
queryset = EventMetaProperty.objects.none()
write_permission = 'organizer.settings.general:write'
def get_queryset(self):
return self.request.organizer.meta_properties.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer
return ctx
@transaction.atomic()
def perform_destroy(self, instance):
instance.log_action(
'pretix.property.deleted',
user=self.request.user,
auth=self.request.auth,
data={'id': instance.pk}
)
instance.delete()
@transaction.atomic()
def perform_create(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.created',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
@transaction.atomic()
def perform_update(self, serializer):
inst = serializer.save(organizer_id=self.request.organizer.pk)
serializer.instance.log_action(
'pretix.property.changed',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
)
return inst
-4
View File
@@ -40,7 +40,6 @@ with scopes_disabled():
class VoucherFilter(FilterSet): class VoucherFilter(FilterSet):
active = BooleanFilter(method='filter_active') active = BooleanFilter(method='filter_active')
code = CharFilter(lookup_expr='iexact') code = CharFilter(lookup_expr='iexact')
search = CharFilter(method='search_qs')
class Meta: class Meta:
model = Voucher model = Voucher
@@ -55,9 +54,6 @@ with scopes_disabled():
return queryset.filter(Q(redeemed__gte=F('max_usages')) | return queryset.filter(Q(redeemed__gte=F('max_usages')) |
(Q(valid_until__isnull=False) & Q(valid_until__lte=now()))) (Q(valid_until__isnull=False) & Q(valid_until__lte=now())))
def search_qs(self, qs, name, value):
return qs.filter(Q(code__icontains=value) | Q(tag__icontains=value) | Q(comment__icontains=value))
class VoucherViewSet(viewsets.ModelViewSet): class VoucherViewSet(viewsets.ModelViewSet):
serializer_class = VoucherSerializer serializer_class = VoucherSerializer
+3 -7
View File
@@ -27,7 +27,7 @@ from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import List, Optional, Protocol from typing import List, Optional, Protocol
from django.conf import settings import sentry_sdk
from django.db import DatabaseError, transaction from django.db import DatabaseError, transaction
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 _
@@ -236,9 +236,7 @@ class OutboundSyncProvider:
# model changes saved by set_sync_error / clear_in_flight calls below # model changes saved by set_sync_error / clear_in_flight calls below
if sq.failed_attempts >= self.max_attempts: if sq.failed_attempts >= self.max_attempts:
logger.exception('Failed to sync order (max attempts exceeded)') logger.exception('Failed to sync order (max attempts exceeded)')
if settings.SENTRY_ENABLED: sentry_sdk.capture_exception(e)
import sentry_sdk
sentry_sdk.capture_exception(e)
sq.set_sync_error("exceeded", e.messages, e.full_message) sq.set_sync_error("exceeded", e.messages, e.full_message)
else: else:
logger.info( logger.info(
@@ -249,9 +247,7 @@ class OutboundSyncProvider:
sq.clear_in_flight() sq.clear_in_flight()
except Exception as e: except Exception as e:
logger.exception('Failed to sync order (unhandled exception)') logger.exception('Failed to sync order (unhandled exception)')
if settings.SENTRY_ENABLED: sentry_sdk.capture_exception(e)
import sentry_sdk
sentry_sdk.capture_exception(e)
sq.set_sync_error("internal", [], str(e)) sq.set_sync_error("internal", [], str(e))
@cached_property @cached_property
+4 -27
View File
@@ -54,7 +54,6 @@ from ...control.forms.filter import get_all_payment_providers
from ...helpers import GroupConcat from ...helpers import GroupConcat
from ...helpers.iter import chunked_iterable from ...helpers.iter import chunked_iterable
from ..exporter import BaseExporter, MultiSheetListExporter from ..exporter import BaseExporter, MultiSheetListExporter
from ..invoicing.transmission import get_transmission_types
from ..services.export import ExportError from ..services.export import ExportError
from ..services.invoices import invoice_pdf_task from ..services.invoices import invoice_pdf_task
from ..signals import ( from ..signals import (
@@ -198,7 +197,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
def iterate_sheet(self, form_data, sheet): def iterate_sheet(self, form_data, sheet):
_ = gettext _ = gettext
if sheet == 'invoices': if sheet == 'invoices':
headers = [ yield [
_('Invoice number'), _('Invoice number'),
_('Date'), _('Date'),
_('Order code'), _('Order code'),
@@ -231,18 +230,8 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
_('Total value (without taxes)'), _('Total value (without taxes)'),
_('Payment matching IDs'), _('Payment matching IDs'),
_('Payment providers'), _('Payment providers'),
_('Transmission type'),
_('Transmission status'),
_('Transmission date'),
] ]
transmission_types = get_transmission_types()
for tt in transmission_types:
for c in tt.describe_info_columns():
headers.append(str(tt.verbose_name) + ': ' + str(c))
yield headers
p_providers = OrderPayment.objects.filter( p_providers = OrderPayment.objects.filter(
order=OuterRef('order'), order=OuterRef('order'),
state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED, state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED,
@@ -253,7 +242,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
'm' 'm'
).order_by() ).order_by()
base_qs = self.invoices_queryset(form_data) base_qs = self.invoices_queryset(form_data)\
qs = base_qs.select_related( qs = base_qs.select_related(
'order', 'refers' 'order', 'refers'
@@ -291,7 +280,7 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
if mid: if mid:
pmis.append(mid) pmis.append(mid)
pmi = '\n'.join(pmis) pmi = '\n'.join(pmis)
line = [ yield [
i.full_invoice_no, i.full_invoice_no,
date_format(i.date, "SHORT_DATE_FORMAT"), date_format(i.date, "SHORT_DATE_FORMAT"),
i.order.code, i.order.code,
@@ -326,20 +315,8 @@ class InvoiceDataExporter(InvoiceExporterMixin, MultiSheetListExporter):
', '.join([ ', '.join([
str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(','))) str(self.providers.get(p, p)) for p in sorted(set((i.payment_providers or '').split(',')))
if p and p != 'free' if p and p != 'free'
]), ])
i.transmission_type_instance.verbose_name,
i.get_transmission_status_display(),
date_format(i.transmission_date, "SHORT_DATETIME_FORMAT") if i.transmission_date else "",
] ]
for tt in transmission_types:
if tt.identifier == i.transmission_type:
described = dict(tt.describe_info(i.invoice_to_transmission_info, i.invoice_to_country, i.invoice_to_is_business))
for c in tt.describe_info_columns():
line.append(described.get(c, ""))
else:
for c in tt.describe_info_columns():
line.append("")
yield line
elif sheet == 'lines': elif sheet == 'lines':
yield [ yield [
_('Invoice number'), _('Invoice number'),
+11 -20
View File
@@ -350,22 +350,16 @@ class WrappedPhonePrefixSelect(Select):
return super().render(name, value or self.initial, *args, **kwargs) return super().render(name, value or self.initial, *args, **kwargs)
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
# self.choices is lazy evaluated, needs to be realized to be modifiable if value and self.choices[1][0] != value:
choices = list(self.choices) matching_choices = len([1 for p, c in self.choices if p == value])
if value and choices[1][0] != value:
matching_choices = len([1 for p, c in choices if p == value])
if matching_choices > 1: if matching_choices > 1:
# Some countries share a phone prefix, for example +1 is used all over the Americas. # Some countries share a phone prefix, for example +1 is used all over the Americas.
# This causes a UX problem: If the default value or the existing data is +12125552368, # This causes a UX problem: If the default value or the existing data is +12125552368,
# the widget will just show the first <option> entry with value="+1" as selected, # the widget will just show the first <option> entry with value="+1" as selected,
# which alphabetically is America Samoa, although most numbers statistically are from # which alphabetically is America Samoa, although most numbers statistically are from
# the US. As a workaround, we detect this case and add an additional choice value with # the US. As a workaround, we detect this case and add an aditional choice value with
# just <option value="+1">+1</option> without an explicit country. # just <option value="+1">+1</option> without an explicit country.
self.choices = [ self.choices.insert(1, (value, value))
choices[0],
(value, value),
*choices[1:],
]
context = super().get_context(name, value, attrs) context = super().get_context(name, value, attrs)
return context return context
@@ -905,7 +899,7 @@ class BaseQuestionsForm(forms.Form):
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values)) field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values))
if q.type != 'M': if q.type != 'M':
field.widget.attrs['required'] = q.required and not self.all_optional field.widget.attrs['required'] = q.required and not self.all_optional
field._required = q.required and not self.all_optional field._required = q.required and not self.all_optional
field.required = False field.required = False
return field return field
@@ -1202,9 +1196,8 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
return field return field
def clean(self): def clean(self):
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time from pretix.base.addressvalidation import \
validate_address, validate_address # local import to prevent impact on startup time
)
d = super().clean() d = super().clean()
@@ -1445,9 +1438,8 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.fields['transmission_type'].widget.attrs['data-trigger-address-info'] = 'on' self.fields['transmission_type'].widget.attrs['data-trigger-address-info'] = 'on'
def clean(self): def clean(self):
from pretix.base.addressvalidation import ( # local import to prevent impact on startup time from pretix.base.addressvalidation import \
validate_address, validate_address # local import to prevent impact on startup time
)
data = self.cleaned_data data = self.cleaned_data
@@ -1501,12 +1493,11 @@ class BaseInvoiceAddressForm(forms.ModelForm):
"vat_id": _("This field is required.") "vat_id": _("This field is required.")
}) })
if self.validate_vat_id and self.instance.vat_id_validated and not any(v in self.changed_data for v in ('is_business', 'vat_id', 'country')): if self.validate_vat_id and self.instance.vat_id_validated and 'vat_id' not in self.changed_data:
pass # Skip re-validation if it is validated pass # Skip re-validation if it is validated
elif self.validate_vat_id and vat_id_applicable: elif self.validate_vat_id and vat_id_applicable:
try: try:
requester_id = self.request.event.settings.invoice_address_from_vat_id normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')), requester_id)
self.instance.vat_id_validated = bool(normalized_id) self.instance.vat_id_validated = bool(normalized_id)
self.instance.vat_id = data['vat_id'] = normalized_id self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e: except VATIDFinalError as e:
+2 -2
View File
@@ -82,8 +82,8 @@ class UserSettingsForm(forms.ModelForm):
class User2FADeviceAddForm(forms.Form): class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64) name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=( devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('otp_totp.totpdevice', _('Smartphone with the Authenticator application')), ('totp', _('Smartphone with the Authenticator application')),
('pretixbase.webauthndevice', _('WebAuthn-compatible hardware token (e.g. Yubikey)')), ('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
)) ))
@@ -107,9 +107,6 @@ class TransmissionType:
def transmission_info_to_form_data(self, transmission_info: dict) -> dict: def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
return transmission_info return transmission_info
def describe_info_columns(self):
return [f.label for f in self.invoice_address_form_fields.values()]
def describe_info(self, transmission_info: dict, country: Country, is_business: bool): def describe_info(self, transmission_info: dict, country: Country, is_business: bool):
form_data = self.transmission_info_to_form_data(transmission_info) form_data = self.transmission_info_to_form_data(transmission_info)
data = [] data = []
@@ -1,49 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-21 11:30
import django.db.models.deletion
from django.db import migrations, models
def fix_unshredded_invoices(apps, _):
Invoice = apps.get_model("pretixbase", "Invoice")
InvoiceLine = apps.get_model("pretixbase", "InvoiceLine")
ignore_fields = (
# bool/int fields are not listed and skipped automatically
'prefix', 'invoice_no', 'full_invoice_no', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id',
'invoice_from_vat_id', 'locale', 'payment_provider_stamp', 'footer_text', 'foreign_currency_display',
'foreign_currency_source', 'transmission_type', 'transmission_provider', 'transmission_status',
)
for i in Invoice.objects.filter(shredded=True):
for f in Invoice._meta.fields:
if f.name in ignore_fields:
continue
val = getattr(i, f.name, None)
if val and isinstance(val, str):
setattr(i, f.name, "█")
elif val and isinstance(val, list): # jsonfield
setattr(i, f.name, [])
elif val and isinstance(val, dict): # jsonfield
setattr(i, f.name, {"_shredded": True})
i.save()
InvoiceLine.objects.filter(
attendee_name__isnull=False,
invoice__shredded=True
).update(attendee_name="█")
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0310_question_valid_string_length_min"),
]
operations = [
migrations.RunPython(
fix_unshredded_invoices,
migrations.RunPython.noop,
),
]
-1
View File
@@ -166,7 +166,6 @@ class Device(LoggedModel):
) )
security_profile = models.CharField( security_profile = models.CharField(
max_length=190, max_length=190,
verbose_name=_('Security profile'),
default='full', default='full',
null=True, null=True,
blank=False blank=False
+40 -7
View File
@@ -626,14 +626,47 @@ class Order(LockModel, LoggedModel):
self.save(update_fields=['last_modified']) self.save(update_fields=['last_modified'])
def set_expires(self, now_dt=None, subevents=None): def set_expires(self, now_dt=None, subevents=None):
from pretix.base.services.payment import compute_payment_deadline now_dt = now_dt or now()
tz = ZoneInfo(self.event.settings.timezone)
self.expires = compute_payment_deadline( sales_channel_suffix = "_" + self.sales_channel.identifier.replace(".", "_")
event=self.event, if not (mode := self.event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
sales_channel=self.sales_channel, mode = self.event.settings.get('payment_term_mode')
now_dt=now_dt, sales_channel_suffix = ""
subevents=subevents,
) if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
self.expires = exp_by_date
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents and subevents:
terms = [
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return
term_last = min(terms)
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < self.expires:
self.expires = term_last
@cached_property @cached_property
def tax_total(self): def tax_total(self):
+34 -40
View File
@@ -801,18 +801,6 @@ def generate_compressed_addon_list(op, order, event, only_checked_in=False):
return addonlist return addonlist
def get_sizebox(page: pypdf.PageObject):
mediabox = page.mediabox
cropbox = page.cropbox
return pypdf.generic.RectangleObject((
max(mediabox[0], cropbox[0]),
max(mediabox[1], cropbox[1]),
min(mediabox[2], cropbox[2]),
min(mediabox[3], cropbox[3]),
))
class Renderer: class Renderer:
def __init__(self, event, layout, background_file): def __init__(self, event, layout, background_file):
@@ -1165,10 +1153,11 @@ class Renderer:
elif o['type'] == "poweredby": elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o) self._draw_poweredby(canvas, op, o)
if self.bg_pdf: if self.bg_pdf:
first_page = self.bg_pdf.pages[0] page_size = (
sizebox = get_sizebox(first_page) self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
page_size = (sizebox.width, sizebox.height) self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
if first_page.rotation in (90, 270): )
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
# swap dimensions due to pdf being rotated # swap dimensions due to pdf being rotated
page_size = page_size[::-1] page_size = page_size[::-1]
canvas.setPageSize(page_size) canvas.setPageSize(page_size)
@@ -1243,7 +1232,9 @@ class Renderer:
for i, page in enumerate(fg_pdf.pages): for i, page in enumerate(fg_pdf.pages):
bg_page = self.bg_pdf.pages[i] bg_page = self.bg_pdf.pages[i]
_merge_with_correct_page_media_box(output, page, bg_page) _correct_page_media_box(bg_page)
page.merge_page(bg_page, over=False)
output.add_page(page)
# pdf_header is a string like "%pdf-X.X" # pdf_header is a string like "%pdf-X.X"
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]): if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
@@ -1308,36 +1299,39 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
bg_pdf.write(bg_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 = bg_pdf.pages[i] bg_page = bg_pdf.pages[i]
_merge_with_correct_page_media_box(output, page, bg_page) _correct_page_media_box(bg_page)
page.merge_page(bg_page, over=False)
# pdf_header is a string like "%pdf-X.X" # pdf_header is a string like "%pdf-X.X"
output.pdf_header = ( if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
bg_pdf.pdf_header fg_pdf.pdf_header = bg_pdf.pdf_header
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:])
else fg_pdf.pdf_header fg_pdf.write(out_file)
)
output.write(out_file)
def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject): def _correct_page_media_box(page: pypdf.PageObject):
""" if page.rotation != 0:
Adds fg_page to output, merging bg_page behind it. page.transfer_rotation_to_content()
media_box = page.mediabox
If bg_page has a non-zero mergebox/cropbox or is rotated via /Rotate, a transformation is applied to fix this."""
trsf = pypdf.Transformation() trsf = pypdf.Transformation()
if bg_page.rotation != 0: if media_box.bottom != 0:
trsf = trsf.rotate(-bg_page.rotation) trsf = trsf.translate(0, -media_box.bottom)
if media_box.left != 0:
mb = get_sizebox(bg_page) trsf = trsf.translate(-media_box.left, 0)
pt1 = trsf.apply_on(mb.lower_left) page.add_transformation(trsf, False)
pt2 = trsf.apply_on(mb.upper_right) for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]:
trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1])) if b in page:
rr = pypdf.generic.RectangleObject(page[b])
fg_page = output.add_page(fg_page) pt1 = trsf.apply_on(rr.lower_left)
fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False) pt2 = trsf.apply_on(rr.upper_right)
page[pypdf.generic.NameObject(b)] = pypdf.generic.RectangleObject((
min(pt1[0], pt2[0]),
min(pt1[1], pt2[1]),
max(pt1[0], pt2[0]),
max(pt1[1], pt2[1]),
))
@deconstructible @deconstructible
-1
View File
@@ -1605,7 +1605,6 @@ def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None,
'max_value': str(max_value) if max_value is not None else None, 'max_value': str(max_value) if max_value is not None else None,
'info_data': info_data or {}, 'info_data': info_data or {},
}) })
cart_session['payments_postpone'] = False
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None): def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
+3 -18
View File
@@ -961,7 +961,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress, def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
meta_info: dict, event: Event, sales_channel: SalesChannel, require_approval=False): meta_info: dict, event: Event, require_approval=False):
fees = [] fees = []
# Pre-rounding, pre-fee total is used for fee calculation # Pre-rounding, pre-fee total is used for fee calculation
total = sum([c.gross_price_before_rounding for c in positions]) total = sum([c.gross_price_before_rounding for c in positions])
@@ -1021,14 +1021,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
payments_assigned += to_pay payments_assigned += to_pay
p['payment_amount'] = to_pay p['payment_amount'] = to_pay
allow_postponed_payment = ( if total != payments_assigned and not require_approval:
require_approval or
(
sales_channel.identifier in event.settings.payment_choice_postpone_allowed_channels and not payment_requests
)
)
if total != payments_assigned and not allow_postponed_payment:
raise OrderError(_("The selected payment methods do not cover the total balance.")) raise OrderError(_("The selected payment methods do not cover the total balance."))
return fees return fees
@@ -1050,15 +1043,7 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
# Final calculation of fees, also performs final rounding # Final calculation of fees, also performs final rounding
try: try:
fees = _apply_rounding_and_fees( fees = _apply_rounding_and_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
positions,
payment_requests,
address,
meta_info,
event,
sales_channel=sales_channel,
require_approval=require_approval
)
except TaxRule.SaleNotAllowed: except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked']) raise OrderError(error_messages['country_blocked'])
-76
View File
@@ -1,76 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
from django.utils.timezone import make_aware, now
from pretix.base.models import Event, SalesChannel
from pretix.base.reldate import RelativeDateWrapper
def compute_payment_deadline(event: Event, sales_channel: SalesChannel, now_dt=None, subevents=None) -> datetime:
now_dt = now_dt or now()
tz = ZoneInfo(event.settings.timezone)
sales_channel_suffix = "_" + sales_channel.identifier.replace(".", "_")
if not (mode := event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
mode = event.settings.get('payment_term_mode')
sales_channel_suffix = ""
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(
days=event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(
minutes=event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
expires = exp_by_date
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if event.has_subevents and subevents:
terms = [
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return expires
term_last = min(terms)
else:
term_last = term_last.datetime(event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < expires:
return term_last
return expires
+2 -68
View File
@@ -343,66 +343,6 @@ def _validate_vat_id_EU(vat_id, country_code):
return vat_id return vat_id
def _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id):
# We can skip most static validation checks because _validate_vat_id_EU always runs before
vat_id = normalize_vat_id(vat_id, country_code)
# The VIES service of the European commission is overused and down due to rate limits A LOT. There is another
# API by German BZSt, but it only works if the requester is German and the requested is not.
# https://www.bzst.de/DE/Unternehmen/Identifikationsnummern/Umsatzsteuer-Identifikationsnummer/AuslaendischeUSt-IdNr/auslaendische_ust_idnr_node.html
try:
r = requests.post(
"https://api.evatr.vies.bzst.de/app/v1/abfrage",
json={
"anfragendeUstid": requester_id,
"angefragteUstid": vat_id,
},
timeout=10,
)
d = r.json()
if r.status_code == 200:
if d['status'] in ('evatr-0000', 'evatr-2008'):
# evatr-0000: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
# evatr-2008: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
# Für die qualifizierte Bestätigungsanfrage liegt einer Besonderheit vor.
# Für Rückfragen wenden Sie sich an das BZSt.
return vat_id
# evatr-2002: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht gültig.
# Sie ist erst gültig ab dem Datum im Feld gueltigAb.
# evatr-2006: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
# Sie war gültig im Zeitraum, der durch die Werte in den Feldern gueltigAb und gueltigBis beschrieben ist.
raise VATIDFinalError(error_messages['invalid'])
elif r.status_code == 400:
if d['status'] in ('evatr-0002', 'evatr-0004', 'evatr-0008'):
# evatr-0002: Mindestens eins der Pflichtfelder ist nicht besetzt.
# evatr-0004: Die anfragende DE Ust-IdNr. ist syntaktisch falsch. Sie passt nicht in das deutsche Erzeugungsschema.
# evatr-0008: Die maximale Anzahl von qualifizierten Bestätigungsabfragen für diese Session wurde erreicht.
# Bitte starten Sie erneut mit einer einfachen Bestätigungsabfrage.
raise VATIDTemporaryError(error_messages['unavailable'])
# evatr-0005: Die angegebene angefragte Ust-IdNr. ist syntaktisch falsch.
# evatr-0012: Die angefrage USt-IdNr. ist syntaktisch falsch. Sie passt nicht in das Erzeugungsschema.
# evatr-2003: Das angegebene Länderkennzeichen der angefragten USt-IdNr. ist nicht gültig.
raise VATIDFinalError(error_messages['invalid'])
elif r.status_code == 403:
# evatr-0006: Die anfragende DE USt-IdNr. ist nicht berechtigt eine DE Ust-IdNr. anzufragen.
# evatr-0007: Fehlerhafter Aufruf.
raise VATIDTemporaryError(error_messages['unavailable'])
elif r.status_code == 404:
if d['status'] in ('evatr-2005'):
# evatr-2005: Die angegebene eigene DE Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
raise VATIDTemporaryError(error_messages['unavailable'])
# evatr-2001: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht vergeben.
raise VATIDFinalError(error_messages['invalid'])
else: # 500, 503
raise VATIDTemporaryError(error_messages['unavailable'])
except requests.RequestException:
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(error_messages['unavailable'])
except ValueError: # JSON parsing failed
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(error_messages['unavailable'])
def _validate_vat_id_CH(vat_id, country_code): def _validate_vat_id_CH(vat_id, country_code):
if vat_id[:3] != 'CHE': if vat_id[:3] != 'CHE':
raise VATIDFinalError(error_messages['country_mismatch']) raise VATIDFinalError(error_messages['country_mismatch'])
@@ -454,18 +394,12 @@ def _validate_vat_id_CH(vat_id, country_code):
return vat_id return vat_id
def validate_vat_id(vat_id, country_code, requester_id=None): def validate_vat_id(vat_id, country_code):
if not vat_id: if not vat_id:
return vat_id return vat_id
country_code = str(country_code) country_code = str(country_code)
if is_eu_country(country_code): if is_eu_country(country_code):
try: return _validate_vat_id_EU(vat_id, country_code)
return _validate_vat_id_EU(vat_id, country_code)
except VATIDTemporaryError:
if requester_id and requester_id.startswith("DE") and not vat_id.startswith("DE"):
return _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id)
else:
raise
elif country_code == 'CH': elif country_code == 'CH':
return _validate_vat_id_CH(vat_id, country_code) return _validate_vat_id_CH(vat_id, country_code)
elif country_code == 'NO': elif country_code == 'NO':
File diff suppressed because one or more lines are too long
+10 -22
View File
@@ -50,8 +50,8 @@ from pretix.api.serializers.order import (
from pretix.api.serializers.waitinglist import WaitingListSerializer from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.i18n import LazyLocaleException from pretix.base.i18n import LazyLocaleException
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, Invoice, InvoiceAddress, CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment,
OrderPayment, OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer, OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
) )
from pretix.base.services.invoices import invoice_pdf_task from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.signals import register_data_shredders from pretix.base.signals import register_data_shredders
@@ -598,30 +598,18 @@ class InvoiceShredder(BaseDataShredder):
def shred_data(self, progress_callback=None): def shred_data(self, progress_callback=None):
qs_i = self.event.invoices.filter(shredded=False) qs_i = self.event.invoices.filter(shredded=False)
total = qs_i.count() total = qs_i.count()
ignore_fields = (
'prefix', 'invoice_no', 'full_invoice_no', 'invoice_from', 'invoice_from_name', 'invoice_from_zipcode',
'invoice_from_city', 'invoice_from_state', 'invoice_from_country', 'invoice_from_tax_id',
'invoice_from_vat_id', 'locale', 'payment_provider_stamp', 'footer_text', 'foreign_currency_display',
'foreign_currency_source', 'transmission_type', 'transmission_provider', 'transmission_status',
)
for i in _progress_helper(qs_i, progress_callback, 0, total): for i in _progress_helper(qs_i, progress_callback, 0, total):
if i.file: if i.file:
i.file.delete() i.file.delete()
i.shredded = True i.shredded = True
i.introductory_text = "█"
for f in Invoice._meta.fields: i.additional_text = "█"
if f.name in ignore_fields: i.invoice_to = "█"
continue i.payment_provider_text = "█"
val = getattr(i, f.name, None) i.transmission_info = {"_shredded": True}
if val and isinstance(val, str): i.save()
setattr(i, f.name, "█") i.lines.update(description="█")
elif val and isinstance(val, list): # jsonfield
setattr(i, f.name, [])
elif val and isinstance(val, dict): # jsonfield
setattr(i, f.name, {"_shredded": True})
i.save()
i.lines.update(description="█", attendee_name="█")
class CachedTicketShredder(BaseDataShredder): class CachedTicketShredder(BaseDataShredder):
+23 -52
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
from decimal import ROUND_HALF_UP, Decimal from decimal import ROUND_HALF_UP, Decimal
from typing import Optional
from babel import Locale, UnknownLocaleError from babel import Locale, UnknownLocaleError
from babel.numbers import format_currency from babel.numbers import format_currency
@@ -36,32 +35,32 @@ register = template.Library()
@register.filter("money") @register.filter("money")
def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_currency=False): def money_filter(value: Decimal, arg='', hide_currency=False):
if isinstance(value, (float, int, str)): if isinstance(value, (float, int)):
if value == '':
return value
value = Decimal(value) value = Decimal(value)
if value is None: if value is None:
value = Decimal('0.00') value = Decimal('0.00')
if not isinstance(value, Decimal): if not isinstance(value, Decimal):
if value == '':
return value
raise TypeError("Invalid data type passed to money filter: %r" % type(value)) raise TypeError("Invalid data type passed to money filter: %r" % type(value))
if not arg: if not arg:
raise ValueError("No currency passed.") raise ValueError("No currency passed.")
arg = arg.upper() arg = arg.upper()
if value.normalize().as_tuple().exponent < -9: places = settings.CURRENCY_PLACES.get(arg, 2)
# Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
# do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations if places < 2 and rounded != value:
# are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python # We display decimal places even if we shouldn't for this currency if rounding
# land to this filter (even though it should not happen). # would make the numbers incorrect. If this branch executes, it's likely a bug in
value = value.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize() # pretix, but we won't show wrong numbers!
if hide_currency:
currency_places = settings.CURRENCY_PLACES.get(arg, 2) return floatformat(value, "2g")
required_places = -value.normalize().as_tuple().exponent else:
render_places = max(currency_places, required_places) return '{} {}'.format(arg, floatformat(value, "2g"))
if hide_currency: if hide_currency:
return floatformat(value, f"{render_places}g") return floatformat(value, f"{places}g")
try: try:
locale = Locale(get_babel_locale()) locale = Locale(get_babel_locale())
@@ -69,29 +68,14 @@ def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_curr
locale = "en" locale = "en"
try: try:
return format_currency( return format_currency(value, arg, locale=locale)
value,
arg,
locale=locale,
# We only allow Babel to restrict the digits to the digits defined by the currency if this does not remove any
# precision in case we have sub-currency precision (which we shouldn't have in most places, but it's still
# better than showing wrong data). Note: Weird precision effects can occur after in-database arithmetic
# on SQLite, since SQLite does not have fixed-decimal computation.
currency_digits=currency_places >= required_places,
decimal_quantization=currency_places >= required_places,
)
except: except:
return '{} {}'.format(arg, floatformat(value, f"{render_places}g")) return '{} {}'.format(arg, floatformat(value, f"{places}g"))
@register.filter("money_without_currency")
def money_filter_without_currency(value: Optional[Decimal | float | int | str], arg=''):
return money_filter(value, arg, hide_currency=True)
@register.filter("money_numberfield") @register.filter("money_numberfield")
def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg=''): def money_numberfield_filter(value: Decimal, arg=''):
if isinstance(value, (float, int, str)): if isinstance(value, (float, int)):
value = Decimal(value) value = Decimal(value)
if not isinstance(value, Decimal): if not isinstance(value, Decimal):
raise TypeError("Invalid data type passed to money filter: %r" % type(value)) raise TypeError("Invalid data type passed to money filter: %r" % type(value))
@@ -103,28 +87,15 @@ def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg='
@register.filter(is_safe=True) @register.filter(is_safe=True)
def tax_rate_format(number: Optional[Decimal | float | int | str]): def tax_rate_format(number):
""" """
Display a Decimal to its significant decimal places, used for tax rates. Display a Decimal to its significant decimal places, used for tax rates.
""" """
if isinstance(number, (float, int, str)): assert isinstance(number, Decimal)
if number == '':
return number
number = Decimal(number)
if number is None:
number = Decimal('0.00')
if not isinstance(number, Decimal):
raise TypeError("Invalid data type passed to tax rate format filter: %r" % type(number))
if number.normalize().as_tuple().exponent < -9:
# Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we
# do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations
# are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python
# land to this filter (even though it should not happen).
number = number.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize()
return mark_safe( return mark_safe(
formats.number_format( formats.number_format(
number, number.normalize(),
-number.normalize().as_tuple().exponent, -number.as_tuple().exponent,
use_l10n=True, use_l10n=True,
force_grouping=False, force_grouping=False,
) )
+2 -7
View File
@@ -19,7 +19,6 @@
# 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/>.
# #
from django.conf import settings
from django.http import ( from django.http import (
HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError, HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError,
) )
@@ -28,6 +27,7 @@ from django.template import TemplateDoesNotExist, loader
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.functional import Promise from django.utils.functional import Promise
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from sentry_sdk import last_event_id
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.middleware import get_language_from_request from pretix.base.middleware import get_language_from_request
@@ -106,14 +106,9 @@ def server_error(request):
template = loader.get_template('500.html') template = loader.get_template('500.html')
except TemplateDoesNotExist: except TemplateDoesNotExist:
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html') return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
if settings.SENTRY_ENABLED:
from sentry_sdk import last_event_id
sentry_id = last_event_id()
else:
sentry_id = None
r = HttpResponseServerError(template.render({ r = HttpResponseServerError(template.render({
'request': request, 'request': request,
'sentry_event_id': sentry_id, 'sentry_event_id': last_event_id(),
})) }))
r.xframe_options_exempt = True r.xframe_options_exempt = True
return r return r
-2
View File
@@ -135,8 +135,6 @@ class BaseQuestionsViewMixin:
question_field.initial = getattr(question_field, 'initial', None) or src['initial'] question_field.initial = getattr(question_field, 'initial', None) or src['initial']
if 'validators' in src: if 'validators' in src:
question_field.validators += src['validators'] question_field.validators += src['validators']
if 'label' in src:
question_field.label = src['label']
if len(form.fields) > 0: if len(form.fields) > 0:
formlist.append(form) formlist.append(form)
+1 -12
View File
@@ -172,9 +172,7 @@ class CachedFileInput(forms.ClearableFileInput):
from ...base.models import CachedFile from ...base.models import CachedFile
v = super().value_from_datadict(data, files, name) v = super().value_from_datadict(data, files, name)
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first() return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
if not v.allowed_for_session(self.request):
v = None
return v return v
def get_context(self, name, value, attrs): def get_context(self, name, value, attrs):
@@ -246,11 +244,6 @@ class ExtFileField(ExtValidationMixin, SizeFileField):
class CachedFileField(ExtFileField): class CachedFileField(ExtFileField):
widget = CachedFileInput widget = CachedFileInput
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.widget.request = self.request
def to_python(self, data): def to_python(self, data):
from ...base.models import CachedFile from ...base.models import CachedFile
@@ -278,8 +271,6 @@ class CachedFileField(ExtFileField):
filename=data.name, filename=data.name,
type=data.content_type, type=data.content_type,
) )
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file) cf.file.save(data.name, data.file)
cf.save() cf.save()
data._uploaded_to = cf data._uploaded_to = cf
@@ -303,8 +294,6 @@ class CachedFileField(ExtFileField):
filename=data.name, filename=data.name,
type=data.content_type, type=data.content_type,
) )
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file) cf.file.save(data.name, data.file)
cf.save() cf.save()
data._uploaded_to = cf data._uploaded_to = cf
+7 -19
View File
@@ -400,10 +400,10 @@ class EventMetaValueForm(forms.ModelForm):
if self.disabled: if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly' self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_value(self): def clean_slug(self):
if self.disabled: if self.disabled:
return self.instance.value if self.instance else None return self.instance.value if self.instance else None
return self.cleaned_data['value'] return self.cleaned_data['slug']
class Meta: class Meta:
model = EventMetaValue model = EventMetaValue
@@ -855,21 +855,14 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
'payment_term_accept_late', 'payment_term_accept_late',
'payment_pending_hidden', 'payment_pending_hidden',
'payment_explanation', 'payment_explanation',
'payment_choice_postpone_allowed_channels',
'tax_rule_payment', 'tax_rule_payment',
] ]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
channels = list(self.obj.organizer.sales_channels.all())
self.fields['payment_choice_postpone_allowed_channels'].choices = [
(c.identifier, c.label) for c in channels
if c.type_instance.payment_restrictions_supported
]
self.term_channel_fields = {} self.term_channel_fields = {}
for c in channels: for c in self.obj.organizer.sales_channels.all():
if c.type_instance.payment_restrictions_supported and c.identifier != "web": if c.type_instance.payment_restrictions_supported and c.identifier != "web":
# At the moment, it seems sufficient to allow this for the same channel types as other payment settings # At the moment, it seems sufficient to allow this for the same channel types as other payment settings
# We can always introduce more flags later if needed # We can always introduce more flags later if needed
@@ -1635,15 +1628,10 @@ class MailSettingsForm(FormPlaceholderMixin, SettingsForm):
self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering) self._set_field_placeholders(k, v, rich=k.startswith('mail_text_') and k not in self.plain_rendering)
for k, v in list(self.fields.items()): for k, v in list(self.fields.items()):
if k.endswith('_attendee'): if k.endswith('_attendee') and not event.settings.attendee_emails_asked:
if not event.settings.attendee_emails_asked: # If we don't ask for attendee emails, we can't send them anything and we don't need to clutter
# If we don't ask for attendee emails, we can't send them anything and we don't need to clutter # the user interface with it
# the user interface with it del self.fields[k]
del self.fields[k]
elif 'subject' in k and k.replace("subject", "send") in self.fields:
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("subject", "send")}'
elif 'text' in k and k.replace("text", "send") in self.fields:
v.widget.attrs["data-display-dependency"] = f'#id_{k.replace("text", "send")}'
class TicketSettingsForm(SettingsForm): class TicketSettingsForm(SettingsForm):
+3 -3
View File
@@ -105,12 +105,12 @@ class GlobalSettingsForm(SettingsForm):
domain=settings.SITE_URL domain=settings.SITE_URL
) )
)), )),
('widget_vue2_origins', forms.CharField( ('widget_vite_origins', forms.CharField(
widget=forms.Textarea(attrs={'rows': '3'}), widget=forms.Textarea(attrs={'rows': '3'}),
required=False, required=False,
# Not translated on purpose, this is a temporary feature and contains too many special case words # Not translated on purpose, this is a temporary feature and contains too many special case words
label="Vue2 widget origins", label="Vite widget origins",
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the old vue2-based widget.", help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the new vite-based widget.",
)) ))
]) ])
responses = register_global_settings.send(self) responses = register_global_settings.send(self)
-2
View File
@@ -87,7 +87,6 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')), ('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')), ('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')), ('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')), ('-1', pgettext_lazy('rrule', 'last')),
], ],
required=False required=False
@@ -135,7 +134,6 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')), ('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')), ('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')), ('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')), ('-1', pgettext_lazy('rrule', 'last')),
], ],
required=False required=False
+2 -2
View File
@@ -435,10 +435,10 @@ class SubEventMetaValueForm(forms.ModelForm):
if self.disabled: if self.disabled:
self.fields['value'].widget.attrs['readonly'] = 'readonly' self.fields['value'].widget.attrs['readonly'] = 'readonly'
def clean_value(self): def clean_slug(self):
if self.disabled: if self.disabled:
return self.instance.value if self.instance else None return self.instance.value if self.instance else None
return self.cleaned_data['value'] return self.cleaned_data['slug']
class Meta: class Meta:
model = SubEventMetaValue model = SubEventMetaValue
-5
View File
@@ -717,10 +717,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'), 'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'), 'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'), 'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
'pretix.property.created': _('An organizer meta property has been created.'),
'pretix.property.deleted': _('An organizer meta property has been deleted.'),
'pretix.property.changed': _('An organizer meta property has been changed.'),
'pretix.property.reordered': _('An organizer meta property has been reordered.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'), 'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'), 'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'), 'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
@@ -778,7 +774,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'), 'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'), 'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'), 'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
'pretix.user.settings.2fa.resetdrift': _('Drift and throttle values for two-factor devices have been reset.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to ' 'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'), 'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed ' 'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
+2 -2
View File
@@ -85,8 +85,8 @@ class PermissionMiddleware:
"user.settings.2fa.enable", "user.settings.2fa.enable",
"user.settings.2fa.disable", "user.settings.2fa.disable",
"user.settings.2fa.regenemergency", "user.settings.2fa.regenemergency",
"user.settings.2fa.confirm.otp_totp.totpdevice", "user.settings.2fa.confirm.totp",
"user.settings.2fa.confirm.pretixbase.webauthndevice", "user.settings.2fa.confirm.webauthn",
"user.settings.2fa.delete", "user.settings.2fa.delete",
"user.settings.2fa.leaveteams", "user.settings.2fa.leaveteams",
"auth.logout", "auth.logout",
@@ -45,8 +45,8 @@
</p> </p>
<div class="form-group buttons"> <div class="form-group buttons">
<input type="submit" class="btn btn-large btn-default" value="{% trans "Cancel" %}"/> <input type="submit" class="btn btn-large btn-default" value="Cancel"/>
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/> <input type="submit" class="btn btn-large btn-primary" name="allow" value="Authorize"/>
</div> </div>
</form> </form>
{% else %} {% else %}
@@ -109,7 +109,6 @@
{% bootstrap_form_errors form layout="control" %} {% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.tax_rule_payment layout="control" %} {% bootstrap_field form.tax_rule_payment layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %} {% bootstrap_field form.payment_explanation layout="control" %}
{% bootstrap_field form.payment_choice_postpone_allowed_channels layout="control" %}
</fieldset> </fieldset>
</div> </div>
{% if "event.settings.payment:write" in request.eventpermset %} {% if "event.settings.payment:write" in request.eventpermset %}
@@ -15,7 +15,7 @@
<div> <div>
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" required value="otp_totp.totpdevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "otp_totp.totpdevice" %}checked{% endif %}> <input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br> <strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
@@ -26,7 +26,7 @@
</div> </div>
<div class="big-radio radio"> <div class="big-radio radio">
<label> <label>
<input type="radio" required value="pretixbase.webauthndevice" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "pretixbase.webauthndevice" %}checked{% endif %}> <input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br> <strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
<div class="help-block"> <div class="help-block">
{% blocktrans trimmed %} {% blocktrans trimmed %}
@@ -116,14 +116,14 @@
{% for d in devices %} {% for d in devices %}
<li class="list-group-item"> <li class="list-group-item">
<a class="btn btn-danger btn-xs pull-right flip" <a class="btn btn-danger btn-xs pull-right flip"
href="{% url "control:user.settings.2fa.delete" devicetype=d.model_label device=d.pk %}"> href="{% url "control:user.settings.2fa.delete" devicetype=d.devicetype device=d.pk %}">
Delete Delete
</a> </a>
{% if d.model_label == "otp_totp.totpdevice" %} {% if d.devicetype == "totp" %}
<span class="fa fa-mobile"></span> <span class="fa fa-mobile"></span>
{% elif d.model_label == "pretixbase.webauthndevice" %} {% elif d.devicetype == "webauthn" %}
<span class="fa fa-usb"></span> <span class="fa fa-usb"></span>
{% elif d.model_label == "pretixbase.u2fdevice" %} {% elif d.devicetype == "u2f" %}
<span class="fa fa-usb"></span> <span class="fa fa-usb"></span>
{% endif %} {% endif %}
{{ d.name }} {{ d.name }}
@@ -1,7 +1,6 @@
{% extends "pretixcontrol/base.html" %} {% extends "pretixcontrol/base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load icon %}
{% block title %}{% trans "User" %}{% endblock %} {% block title %}{% trans "User" %}{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "User" %} {{ user.email }}</h1> <h1>{% trans "User" %} {{ user.email }}</h1>
@@ -60,83 +59,8 @@
{% bootstrap_field form.is_verified layout='control' %} {% bootstrap_field form.is_verified layout='control' %}
{% endif %} {% endif %}
{% bootstrap_field form.last_login layout='control' %} {% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.needs_password_change layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %} {% bootstrap_field form.require_2fa layout='control' %}
<div class="form-group"> {% bootstrap_field form.needs_password_change layout='control' %}
<div class="col-md-9 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<button class="btn btn-default btn-xs pull-right" type="submit" form="resetdriftthrottle">
{% trans "Reset drift and throttle" %}
</button>
<h3 class="panel-title">
{% trans "Available two-factor authentication methods" %}
</h3>
</div>
<table class="panel-body table table-hover">
{% for d in devices %}
<tr>
<td>
{% if d.model_label == 'otp_totp.totpdevice' %}
TOTP
{% elif d.model_label == 'pretixbase.u2fdevice' %}
U2F
{% elif d.model_label == 'pretixbase.webauthndevice' %}
WebAuthn
{% elif d.model_label == 'otp_static.staticdevice' %}
{% trans "Emergency tokens" %}
{% endif %}
{% if d.confirmed %}
{% icon "check" %}
{% else %}
{% icon "warning" %}
{% endif %}
</td>
<td>
{{ d.name }}
</td>
<td>
{% if d.throttling_failure_timestamp %}
{% blocktrans trimmed with date=d.throttling_failure_timestamp|date:"SHORT_DATETIME_FORMAT" count cnt=d.throttling_failure_count %}
1 failed attempt since {{ date }}
{% plural %}
{{ cnt }} failed attempts since {{ date }}
{% endblocktrans %}
<br>
{% endif %}
{% if d.throttling_enabled and not d.verify_is_allowed.0 %}
<strong>
{% blocktrans trimmed with date=d.verify_is_allowed.1.locked_until|date:"SHORT_DATETIME_FORMAT" %}
Currently locked until {{ date }}
{% endblocktrans %}
</strong>
<br>
{% endif %}
{% if d.model_label == 'otp_totp.totpdevice' %}
<small>
<code>step = {{ d.step }},
t0 = {{ d.t0 }},
digits = {{ d.digits }},
tolerance = {{ d.tolerance }},
drift = {{ d.drift }},
last_t = {{ d.last_t }}</code>
</small>
{% elif d.model_label == 'pretixbase.u2fdevice' %}
<small>
<code>sign_count = {{ d.sign_count }}</code>
</small>
{% elif d.model_label == 'otp_static.staticdevice' %}
<small>
<code>token_count = {{ d.token_set.count }}</code>
</small>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>{% trans "Team memberships" %}</legend> <legend>{% trans "Team memberships" %}</legend>
@@ -178,8 +102,4 @@
</div> </div>
</div> </div>
</div> </div>
<form action="{% url "control:users.resetdriftthrottle" id=user.pk %}" id="resetdriftthrottle" method="post">
{% csrf_token %}
</form>
{% endblock %} {% endblock %}
+2 -3
View File
@@ -78,7 +78,6 @@ urlpatterns = [
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'), re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'), re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'), re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
re_path(r'^users/(?P<id>\d+)/resetdriftthrottle$', users.Reset2FADriftThrottleView.as_view(), name='users.resetdriftthrottle'),
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'), re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'), re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'), re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
@@ -107,9 +106,9 @@ urlpatterns = [
re_path(r'^settings/2fa/regenemergency', user.User2FARegenerateEmergencyView.as_view(), re_path(r'^settings/2fa/regenemergency', user.User2FARegenerateEmergencyView.as_view(),
name='user.settings.2fa.regenemergency'), name='user.settings.2fa.regenemergency'),
re_path(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(), re_path(r'^settings/2fa/totp/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmTOTPView.as_view(),
name='user.settings.2fa.confirm.otp_totp.totpdevice'), name='user.settings.2fa.confirm.totp'),
re_path(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(), re_path(r'^settings/2fa/webauthn/(?P<device>[0-9]+)/confirm', user.User2FADeviceConfirmWebAuthnView.as_view(),
name='user.settings.2fa.confirm.pretixbase.webauthndevice'), name='user.settings.2fa.confirm.webauthn'),
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(), re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
name='user.settings.2fa.delete'), name='user.settings.2fa.delete'),
re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'), re_path(r'^settings/email/confirm$', user.UserEmailConfirmView.as_view(), name='user.settings.email.confirm'),
+15 -53
View File
@@ -35,7 +35,6 @@
import base64 import base64
import json import json
import logging import logging
import math
import time import time
from urllib.parse import quote, urljoin, urlparse from urllib.parse import quote, urljoin, urlparse
@@ -51,12 +50,11 @@ from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext_lazy as _, ngettext
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_otp import devices_for_user from django_otp import match_token
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from webauthn.helpers import generate_challenge from webauthn.helpers import generate_challenge
@@ -66,7 +64,6 @@ from pretix.base.forms.auth import (
) )
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.helpers import OF_SELF
from pretix.helpers.http import get_client_ip, redirect_to_url from pretix.helpers.http import get_client_ip, redirect_to_url
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
from pretix.helpers.security import handle_login_source, session_login from pretix.helpers.security import handle_login_source, session_login
@@ -398,17 +395,15 @@ class Recover(TemplateView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if self.form.is_valid(): if self.form.is_valid():
with transaction.atomic(): try:
# Check token in transaction to prevent race condition user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native')
try: except User.DoesNotExist:
user = User.objects.select_for_update(of=OF_SELF).get(id=self.request.GET.get('id'), auth_backend='native') return self.invalid('unknownuser')
except User.DoesNotExist: if not default_token_generator.check_token(user, self.request.GET.get('token')):
return self.invalid('unknownuser') return self.invalid('invalid')
if not default_token_generator.check_token(user, self.request.GET.get('token')): user.set_password(self.form.cleaned_data['password'])
return self.invalid('invalid') user.needs_password_change = False
user.set_password(self.form.cleaned_data['password']) user.save()
user.needs_password_change = False
user.save()
messages.success(request, _('You can now login using your new password.')) messages.success(request, _('You can now login using your new password.'))
user.log_action('pretix.control.auth.user.forgot_password.recovered') user.log_action('pretix.control.auth.user.forgot_password.recovered')
@@ -465,7 +460,6 @@ class Login2FAView(TemplateView):
token = request.POST.get('token', '').strip().replace(' ', '') token = request.POST.get('token', '').strip().replace(' ', '')
valid = False valid = False
retry_after = None
if 'webauthn_challenge' in self.request.session and token.startswith('{'): if 'webauthn_challenge' in self.request.session and token.startswith('{'):
challenge = self.request.session['webauthn_challenge'] challenge = self.request.session['webauthn_challenge']
@@ -521,28 +515,12 @@ class Login2FAView(TemplateView):
valid = True valid = True
break break
else: else:
with transaction.atomic(): valid = match_token(self.user, token)
for device in devices_for_user(self.user, for_verify=True): if isinstance(valid, StaticDevice):
if isinstance(device, StaticDevice) and len(token) < 12:
# If we enter a wrong TOTP token (which is 6 characters), do not even try if it is a valid
# emergency token, which will only "lock up" the StaticDevice due to the throttling plugin
# and just locks people out without security gain.
continue
if device.verify_token(token):
valid = True
break
elif hasattr(device, 'verify_is_allowed'):
verify_allowed, reason_dict = device.verify_is_allowed()
if not verify_allowed:
if not retry_after or reason_dict['locked_until'] > retry_after:
retry_after = reason_dict['locked_until']
else:
device = None
if isinstance(device, StaticDevice):
self.user.send_security_notice([ self.user.send_security_notice([
_("A recovery code for two-factor authentification was used to log in.") _("A recovery code for two-factor authentification was used to log in.")
]) ])
if valid: if valid:
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.") logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")
pretix_successful_logins.inc(1) pretix_successful_logins.inc(1)
@@ -555,23 +533,7 @@ class Login2FAView(TemplateView):
return redirect('control:index') return redirect('control:index')
else: else:
pretix_failed_logins.inc(1, reason="2fa") pretix_failed_logins.inc(1, reason="2fa")
msg = _('Invalid code, please try again.') messages.error(request, _('Invalid code, please try again.'))
if retry_after:
seconds = (retry_after - now()).total_seconds()
minutes = seconds / 60
if minutes >= 1:
msg = ngettext(
'Invalid code. Please try again after waiting {value} minute.',
'Invalid code. Please try again after waiting {value} minutes.',
minutes,
).format(value=math.ceil(minutes))
elif seconds >= 1:
msg = ngettext(
'Invalid code. Please try again after waiting {value} second.',
'Invalid code. Please try again after waiting {value} seconds.',
seconds,
).format(value=math.ceil(seconds))
messages.error(request, msg)
return redirect('control:auth.login.2fa') return redirect('control:auth.login.2fa')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
-3
View File
@@ -163,9 +163,6 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
i = modelcopy(self.copy_from) i = modelcopy(self.copy_from)
i.pk = None i.pk = None
kwargs['instance'] = i kwargs['instance'] = i
kwargs["initial"]["limit_sales_channels"] = self.copy_from.limit_sales_channels.all()
kwargs["initial"]["condition_limit_products"] = self.copy_from.condition_limit_products.all()
kwargs["initial"]["benefit_limit_products"] = self.copy_from.benefit_limit_products.all()
else: else:
kwargs['instance'] = Discount(event=self.request.event) kwargs['instance'] = Discount(event=self.request.event)
+5 -22
View File
@@ -1177,8 +1177,6 @@ class OrderRefundView(OrderView):
manual_value = formats.sanitize_separators(manual_value) manual_value = formats.sanitize_separators(manual_value)
try: try:
manual_value = Decimal(manual_value) manual_value = Decimal(manual_value)
if manual_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1208,8 +1206,6 @@ class OrderRefundView(OrderView):
giftcard_value = formats.sanitize_separators(giftcard_value) giftcard_value = formats.sanitize_separators(giftcard_value)
try: try:
giftcard_value = Decimal(giftcard_value) giftcard_value = Decimal(giftcard_value)
if giftcard_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1259,8 +1255,6 @@ class OrderRefundView(OrderView):
offsetting_value = formats.sanitize_separators(offsetting_value) offsetting_value = formats.sanitize_separators(offsetting_value)
try: try:
offsetting_value = Decimal(offsetting_value) offsetting_value = Decimal(offsetting_value)
if offsetting_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1277,9 +1271,6 @@ class OrderRefundView(OrderView):
if offset_order.event.currency != self.request.event.currency: if offset_order.event.currency != self.request.event.currency:
messages.error(self.request, _('You entered an order in an event with a different currency.')) messages.error(self.request, _('You entered an order in an event with a different currency.'))
is_valid = False is_valid = False
if not self.request.user.has_event_permission(self.request.organizer, offset_order.event, 'event.orders:write', request=self.request):
messages.error(self.request, _('You entered an order in an event that you do not have access to.'))
is_valid = False
refunds.append(OrderRefund( refunds.append(OrderRefund(
order=order, order=order,
payment=None, payment=None,
@@ -1295,13 +1286,10 @@ class OrderRefundView(OrderView):
)) ))
for identifier, prov in self.request.event.get_payment_providers().items(): for identifier, prov in self.request.event.get_payment_providers().items():
# prof = process form, not a typo for prov(ider)
prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0' prof_value = self.request.POST.get(f'newrefund-{identifier}', '0') or '0'
prof_value = formats.sanitize_separators(prof_value) prof_value = formats.sanitize_separators(prof_value)
try: try:
prof_value = Decimal(prof_value) prof_value = Decimal(prof_value)
if prof_value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1325,8 +1313,6 @@ class OrderRefundView(OrderView):
value = formats.sanitize_separators(value) value = formats.sanitize_separators(value)
try: try:
value = Decimal(value) value = Decimal(value)
if value < Decimal("0.00"):
raise TypeError("Please do not use negative numbers")
except (DecimalException, TypeError): except (DecimalException, TypeError):
messages.error(self.request, _('You entered an invalid number.')) messages.error(self.request, _('You entered an invalid number.'))
is_valid = False is_valid = False
@@ -1356,12 +1342,7 @@ class OrderRefundView(OrderView):
)) ))
any_success = False any_success = False
if refund_selected != full_refund: if refund_selected == full_refund and is_valid:
messages.error(self.request, _('The refunds you selected do not match the selected total refund '
'amount.'))
is_valid = False
if is_valid:
for r in refunds: for r in refunds:
r.save() r.save()
order.log_action('pretix.event.order.refund.created', { order.log_action('pretix.event.order.refund.created', {
@@ -1433,6 +1414,9 @@ class OrderRefundView(OrderView):
) )
})) }))
return redirect(self.get_order_url()) 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():
@@ -1662,8 +1646,7 @@ class OrderCheckVATID(OrderView):
return redirect(self.get_order_url()) return redirect(self.get_order_url())
try: try:
requester_id = self.request.event.settings.invoice_address_from_vat_id normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
normalized_id = validate_vat_id(ia.vat_id, str(ia.country), requester_id)
with transaction.atomic(): with transaction.atomic():
ia.vat_id_validated = True ia.vat_id_validated = True
ia.vat_id = normalized_id ia.vat_id = normalized_id
+3 -3
View File
@@ -778,9 +778,9 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
# Assumption: Who has access to modify organizer settings may see all events and disable/enable plugins kwargs["events"] = self.request.user.get_events_with_permission(
# for them. Otherwise, inconsistent situations occur. "event.settings.general:write", request=self.request
kwargs["events"] = self.request.organizer.events.all() ).filter(organizer=self.request.organizer)
kwargs["initial"] = { kwargs["initial"] = {
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)') "events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
} }
-5
View File
@@ -27,7 +27,6 @@ from decimal import Decimal
from io import BytesIO from io import BytesIO
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.files import File from django.core.files import File
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
@@ -194,7 +193,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7) c.expires = now() + timedelta(days=7)
c.date = now() c.date = now()
c.filename = 'background_preview.pdf' c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf' c.type = 'application/pdf'
c.save() c.save()
c.file.save('empty.pdf', ContentFile(buffer.read())) c.file.save('empty.pdf', ContentFile(buffer.read()))
@@ -220,7 +218,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7) c.expires = now() + timedelta(days=7)
c.date = now() c.date = now()
c.filename = 'background_preview.pdf' c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf' c.type = 'application/pdf'
c.file = fileobj c.file = fileobj
c.save() c.save()
@@ -306,7 +303,5 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView): class PdfView(TemplateView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf") cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
if not cf.allowed_for_session(request, "ticketoutput-pdf-background"):
raise PermissionDenied()
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf') resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
return resp return resp
-5
View File
@@ -1276,11 +1276,6 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie
self._default_meta = self.request.event.meta_data self._default_meta = self.request.event.meta_data
for p in self.request.organizer.meta_properties.all(): for p in self.request.organizer.meta_properties.all():
if p.protected and not self.request.user.has_organizer_permission(
self.request.organizer, 'organizer.settings.general:write', request=self.request
):
continue
inst = SubEventMetaValue(property=p) inst = SubEventMetaValue(property=p)
if len(matches[p.id]) == 1 and matches[p.id][0]['c'] == total: if len(matches[p.id]) == 1 and matches[p.id][0]['c'] == total:
inst.value = matches[p.id][0]['value'] inst.value = matches[p.id][0]['value']
+24 -14
View File
@@ -59,7 +59,6 @@ from django.utils.translation import gettext_lazy as _
from django.views import View from django.views import View
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.generic import FormView, ListView, TemplateView, UpdateView from django.views.generic import FormView, ListView, TemplateView, UpdateView
from django_otp import devices_for_user
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice from django_otp.plugins.otp_totp.models import TOTPDevice
from django_scopes import scopes_disabled from django_scopes import scopes_disabled
@@ -86,6 +85,7 @@ from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
from pretix.helpers.security import session_reauth from pretix.helpers.security import session_reauth
from pretix.helpers.u2f import websafe_encode from pretix.helpers.u2f import websafe_encode
REAL_DEVICE_TYPES = (TOTPDevice, WebAuthnDevice, U2FDevice)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -313,7 +313,17 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
except StaticDevice.DoesNotExist: except StaticDevice.DoesNotExist:
ctx['static_tokens_device'] = None ctx['static_tokens_device'] = None
ctx['devices'] = [d for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)] ctx['devices'] = []
for dt in REAL_DEVICE_TYPES:
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
for obj in objs:
if dt == TOTPDevice:
obj.devicetype = 'totp'
elif dt == U2FDevice:
obj.devicetype = 'u2f'
elif dt == WebAuthnDevice:
obj.devicetype = 'webauthn'
ctx['devices'] += objs
ctx['obligatory'] = None ctx['obligatory'] = None
if settings.PRETIX_OBLIGATORY_2FA is True: if settings.PRETIX_OBLIGATORY_2FA is True:
@@ -332,9 +342,9 @@ class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
template_name = 'pretixcontrol/user/2fa_add.html' template_name = 'pretixcontrol/user/2fa_add.html'
def form_valid(self, form): def form_valid(self, form):
if form.cleaned_data['devicetype'] == 'otp_totp.totpdevice': if form.cleaned_data['devicetype'] == 'totp':
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name']) dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
elif form.cleaned_data['devicetype'] == 'pretixbase.webauthndevice': elif form.cleaned_data['devicetype'] == 'webauthn':
if not self.request.is_secure(): if not self.request.is_secure():
messages.error(self.request, messages.error(self.request,
_('Security devices are only available if pretix is served via HTTPS.')) _('Security devices are only available if pretix is served via HTTPS.'))
@@ -354,11 +364,11 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
@cached_property @cached_property
def device(self): def device(self):
if self.kwargs['devicetype'] == 'otp_totp.totpdevice': if self.kwargs['devicetype'] == 'totp':
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True) return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
elif self.kwargs['devicetype'] == 'pretixbase.webauthndevice': elif self.kwargs['devicetype'] == 'webauthn':
return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True) return get_object_or_404(WebAuthnDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
elif self.kwargs['devicetype'] == 'pretixbase.u2fdevice': elif self.kwargs['devicetype'] == 'u2f':
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True) return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -376,7 +386,7 @@ class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
msgs = [ msgs = [
_('A two-factor authentication device has been removed from your account.') _('A two-factor authentication device has been removed from your account.')
] ]
if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)): if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
self.request.user.require_2fa = False self.request.user.require_2fa = False
self.request.user.save() self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user) self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
@@ -451,7 +461,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
).first() ).first()
if credential_id_exists: if credential_id_exists:
messages.error(request, _('This security device is already registered.')) messages.error(request, _('This security device is already registered.'))
return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={ return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
'device': self.device.pk 'device': self.device.pk
})) }))
@@ -465,7 +475,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
self.device.save() self.device.save()
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={ self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
'id': self.device.pk, 'id': self.device.pk,
'devicetype': 'pretixbase.webauthndevice', 'devicetype': 'u2f',
'name': self.device.name, 'name': self.device.name,
}) })
notices = [ notices = [
@@ -493,7 +503,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
except Exception: except Exception:
messages.error(request, _('The registration could not be completed. Please try again.')) messages.error(request, _('The registration could not be completed. Please try again.'))
logger.exception('WebAuthn registration failed') logger.exception('WebAuthn registration failed')
return redirect(reverse('control:user.settings.2fa.confirm.pretixbase.webauthndevice', kwargs={ return redirect(reverse('control:user.settings.2fa.confirm.webauthn', kwargs={
'device': self.device.pk 'device': self.device.pk
})) }))
@@ -527,7 +537,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={ self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
'id': self.device.pk, 'id': self.device.pk,
'name': self.device.name, 'name': self.device.name,
'devicetype': 'otp_totp.totpdevice' 'devicetype': 'totp'
}) })
notices = [ notices = [
_('A new two-factor authentication device has been added to your account.') _('A new two-factor authentication device has been added to your account.')
@@ -553,7 +563,7 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
else: else:
messages.error(request, _('The code you entered was not valid. If this problem persists, please check ' messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
'that the date and time of your phone are configured correctly.')) 'that the date and time of your phone are configured correctly.'))
return redirect(reverse('control:user.settings.2fa.confirm.otp_totp.totpdevice', kwargs={ return redirect(reverse('control:user.settings.2fa.confirm.totp', kwargs={
'device': self.device.pk 'device': self.device.pk
})) }))
@@ -584,7 +594,7 @@ class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_enable.html' template_name = 'pretixcontrol/user/2fa_enable.html'
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not any(d.confirmed for d in devices_for_user(self.request.user) if not isinstance(d, StaticDevice)): if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
messages.error(request, _('Please configure at least one device before enabling two-factor ' messages.error(request, _('Please configure at least one device before enabling two-factor '
'authentication.')) 'authentication.'))
return redirect(reverse('control:user.settings.2fa')) return redirect(reverse('control:user.settings.2fa'))
-23
View File
@@ -40,7 +40,6 @@ from django.utils.functional import cached_property
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 ListView, TemplateView from django.views.generic import ListView, TemplateView
from django_otp import devices_for_user
from django_otp.plugins.otp_static.models import StaticDevice from django_otp.plugins.otp_static.models import StaticDevice
from hijack import signals from hijack import signals
@@ -108,9 +107,6 @@ class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationReq
ctx['backend'] = ( ctx['backend'] = (
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
) )
ctx['devices'] = devices_for_user(self.object)
return ctx return ctx
def get_success_url(self): def get_success_url(self):
@@ -187,25 +183,6 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
return reverse('control:users.edit', kwargs=self.kwargs) return reverse('control:users.edit', kwargs=self.kwargs)
class Reset2FADriftThrottleView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
def get(self, request, *args, **kwargs):
return redirect(reverse('control:users.edit', kwargs=self.kwargs))
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
self.object.totpdevice_set.update(drift=0, throttling_failure_timestamp=None, throttling_failure_count=0)
self.object.staticdevice_set.update(throttling_failure_timestamp=None, throttling_failure_count=0)
self.object.log_action('pretix.user.settings.2fa.resetdrift', user=self.request.user)
messages.success(request, _(
'The drift values for TOTP devices have been reset.'
))
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('control:users.edit', kwargs=self.kwargs)
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView): class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
template_name = "pretixcontrol/users/anonymize.html" template_name = "pretixcontrol/users/anonymize.html"
+2 -4
View File
@@ -144,7 +144,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
headers = [ headers = [
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'), _('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'), _('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
_('Comment'), _('Budget'), _('Budget used') _('Comment')
] ]
writer.writerow(headers) writer.writerow(headers)
@@ -170,9 +170,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
str(v.redeemed), str(v.redeemed),
str(v.max_usages), str(v.max_usages),
str(v.seat) if v.seat else "", str(v.seat) if v.seat else "",
str(v.comment) if v.comment else "", str(v.comment) if v.comment else ""
str(v.budget) if v.budget is not None else "",
str(v.budget_used) if v.budget is not None else "",
] ]
writer.writerow(row) writer.writerow(row)
+1 -1
View File
@@ -29,7 +29,7 @@ from django.urls import reverse
def build_absolute_uri(urlname, args=None, kwargs=None): def build_absolute_uri(urlname, args=None, kwargs=None):
warnings.warn( warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. ' 'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with mainreverse_absolute.', 'Replace this usage with ',
DeprecationWarning DeprecationWarning
) )
return mainreverse_absolute(urlname, args, kwargs) return mainreverse_absolute(urlname, args, kwargs)
+23 -30
View File
@@ -8,8 +8,8 @@ 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: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-20 14:00+0000\n" "PO-Revision-Date: 2026-06-25 14:00+0000\n"
"Last-Translator: Rita Gimenez <barcelonamusictech@gmail.com>\n" "Last-Translator: Kim Lozano <joaquim.lozano@upc.edu>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix/"
"ca/>\n" "ca/>\n"
"Language: ca\n" "Language: ca\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 2026.9.1\n" "X-Generator: Weblate 2026.6.1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "English" msgid "English"
@@ -37,11 +37,11 @@ msgstr "Àrab"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Basque" msgid "Basque"
msgstr "Basc" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Catalan" msgid "Catalan"
msgstr "Català" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Chinese (simplified)" msgid "Chinese (simplified)"
@@ -49,11 +49,11 @@ msgstr "Xinès (simplificat)"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Chinese (traditional)" msgid "Chinese (traditional)"
msgstr "Xinès (tradicional)" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Czech" msgid "Czech"
msgstr "Txec" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Croatian" msgid "Croatian"
@@ -81,7 +81,7 @@ msgstr "Finlandès"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Galician" msgid "Galician"
msgstr "Gallec" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Greek" msgid "Greek"
@@ -89,15 +89,15 @@ msgstr "Grec"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Hebrew" msgid "Hebrew"
msgstr "Hebreu" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Hungarian" msgid "Hungarian"
msgstr "Hongarès" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
msgstr "Indonesi" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Italian" msgid "Italian"
@@ -105,7 +105,7 @@ msgstr "Italià"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Japanese" msgid "Japanese"
msgstr "Japonès" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Latvian" msgid "Latvian"
@@ -113,7 +113,7 @@ msgstr "Letó"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Norwegian Bokmål" msgid "Norwegian Bokmål"
msgstr "Noruec" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Polish" msgid "Polish"
@@ -129,7 +129,7 @@ msgstr "Portuguès (Brasil)"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Romanian" msgid "Romanian"
msgstr "Romanès" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Russian" msgid "Russian"
@@ -137,11 +137,11 @@ msgstr "Rus"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Slovak" msgid "Slovak"
msgstr "Eslovac" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Swedish" msgid "Swedish"
msgstr "Suec" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Spanish" msgid "Spanish"
@@ -149,11 +149,11 @@ msgstr "Espanyol"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Spanish (Latin America)" msgid "Spanish (Latin America)"
msgstr "Espanyol (Llatinoamèrica)" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Thai" msgid "Thai"
msgstr "Tailandès" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Turkish" msgid "Turkish"
@@ -161,15 +161,13 @@ msgstr "Turc"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Ukrainian" msgid "Ukrainian"
msgstr "Ucraïnès" msgstr ""
#: pretix/api/auth/devicesecurity.py #: pretix/api/auth/devicesecurity.py
msgid "" msgid ""
"Full device access (reading and changing orders and gift cards, reading of " "Full device access (reading and changing orders and gift cards, reading of "
"products and settings)" "products and settings)"
msgstr "" msgstr ""
"Accés total al dispositiu (lectura i modificació de comandes i targetes "
"regal, lectura de productes i configuració)"
#: pretix/api/auth/devicesecurity.py #: pretix/api/auth/devicesecurity.py
msgid "pretixSCAN" msgid "pretixSCAN"
@@ -311,7 +309,7 @@ msgstr "La cistella és buida."
#: pretix/api/serializers/item.py #: pretix/api/serializers/item.py
msgid "The program end must not be empty." msgid "The program end must not be empty."
msgstr "El final del programa no ha de ser buit." msgstr ""
#: pretix/api/serializers/item.py pretix/base/models/items.py #: pretix/api/serializers/item.py pretix/base/models/items.py
#, fuzzy #, fuzzy
@@ -341,7 +339,7 @@ msgstr ""
#: pretix/api/serializers/item.py #: pretix/api/serializers/item.py
msgid "Only admission products can currently be personalized." msgid "Only admission products can currently be personalized."
msgstr "Actualment, només es poden personalitzar els productes d'accés." msgstr ""
#: pretix/api/serializers/item.py #: pretix/api/serializers/item.py
msgid "" msgid ""
@@ -436,7 +434,7 @@ msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py #: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
#, python-format #, python-format
msgid "You've been invited to join %(organizer)s" msgid "You've been invited to join %(organizer)s"
msgstr "Has estat convidat/da a unir-te a %(organizer)s" msgstr ""
#: pretix/api/serializers/organizer.py pretix/control/views/organizer.py #: pretix/api/serializers/organizer.py pretix/control/views/organizer.py
msgid "This user already has been invited for this team." msgid "This user already has been invited for this team."
@@ -458,7 +456,7 @@ msgstr ""
#: pretix/api/views/checkin.py #: pretix/api/views/checkin.py
msgid "Medium connected to other event" msgid "Medium connected to other event"
msgstr "Mitjà connectat a un altre esdeveniment" msgstr ""
#: pretix/api/views/checkin.py #: pretix/api/views/checkin.py
#, fuzzy #, fuzzy
@@ -644,8 +642,6 @@ msgid ""
"This includes product added or deleted and changes to nested objects like " "This includes product added or deleted and changes to nested objects like "
"variations or bundles." "variations or bundles."
msgstr "" msgstr ""
"Això inclou productes afegits o eliminats i canvis en objectes imbricats, "
"com ara variacions o lots."
#: pretix/api/webhooks.py #: pretix/api/webhooks.py
#, fuzzy #, fuzzy
@@ -658,9 +654,6 @@ msgid ""
"This includes related events like creation, deletion, opening or closing of " "This includes related events like creation, deletion, opening or closing of "
"quotas. No webhook is sent for changes to the resulting availability." "quotas. No webhook is sent for changes to the resulting availability."
msgstr "" msgstr ""
"Això inclou esdeveniments relacionats, com ara la creació, l'eliminació, "
"l'obertura o el tancament de quotes. No s'envia cap webhook per als canvis "
"en la disponibilitat resultant."
#: pretix/api/webhooks.py #: pretix/api/webhooks.py
#, fuzzy #, fuzzy
+14 -7
View File
@@ -5,16 +5,16 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-14 23:00+0000\n" "PO-Revision-Date: 2026-08-24 15:31+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/" "Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
"de/>\n" ">\n"
"Language: de\n" "Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"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 2026.9.1\n" "X-Generator: Weblate 2026.8.1\n"
"X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n" "X-Poedit-Bookmarks: -1,-1,904,-1,-1,-1,-1,-1,-1,-1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
@@ -11708,7 +11708,7 @@ msgid ""
msgstr "" msgstr ""
"Hallo,\n" "Hallo,\n"
"\n" "\n"
"Sie erhalten diese Nachricht, weil Sie einen neuen Link zu Ihrer Bestellung " "Sie erhalten diese Nachricht weil Sie einen neuen Link zu Ihrer Bestellung "
"für\n" "für\n"
"{event} angefordert haben.\n" "{event} angefordert haben.\n"
"\n" "\n"
@@ -11738,8 +11738,8 @@ msgid ""
msgstr "" msgstr ""
"Hallo,\n" "Hallo,\n"
"\n" "\n"
"Sie erhalten diese Nachricht, weil Sie einen neuen Link zu Ihren " "Sie erhalten diese Nachricht weil Sie einen neuen Link zu Ihren Bestellungen "
"Bestellungen für\n" "für\n"
"{event} angefordert haben. Sie finden Ihre Bestellungen unter folgenden " "{event} angefordert haben. Sie finden Ihre Bestellungen unter folgenden "
"Links:\n" "Links:\n"
"\n" "\n"
@@ -31805,6 +31805,13 @@ msgid "Allow further payments during compliance hold"
msgstr "Erlaube weitere Zahlungsversuche während PayPal eine Zahlung überprüft" msgstr "Erlaube weitere Zahlungsversuche während PayPal eine Zahlung überprüft"
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
#, fuzzy
#| msgid ""
#| "PayPals fraud prevention might block processing of individual payments "
#| "for a considerable amount of time. The payment is marked as \"pending\" "
#| "during this time window. You can allow your customers to start another "
#| "payment attempts during that window. This might result in them being "
#| "charged twice if theoriginal payment is approved."
msgid "" msgid ""
"PayPals fraud prevention might block processing of individual payments for a " "PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during " "considerable amount of time. The payment is marked as \"pending\" during "
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n" "PO-Revision-Date: 2026-08-24 15:31+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/de_Informal/>\n" "pretix/pretix/de_Informal/>\n"
@@ -31761,6 +31761,13 @@ msgid "Allow further payments during compliance hold"
msgstr "Erlaube weitere Zahlungsversuche während PayPal eine Zahlung überprüft" msgstr "Erlaube weitere Zahlungsversuche während PayPal eine Zahlung überprüft"
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
#, fuzzy
#| msgid ""
#| "PayPals fraud prevention might block processing of individual payments "
#| "for a considerable amount of time. The payment is marked as \"pending\" "
#| "during this time window. You can allow your customers to start another "
#| "payment attempts during that window. This might result in them being "
#| "charged twice if theoriginal payment is approved."
msgid "" msgid ""
"PayPals fraud prevention might block processing of individual payments for a " "PayPals fraud prevention might block processing of individual payments for a "
"considerable amount of time. The payment is marked as \"pending\" during " "considerable amount of time. The payment is marked as \"pending\" during "
+176 -111
View File
@@ -8,7 +8,7 @@ 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: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n" "PO-Revision-Date: 2026-07-08 16:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n" "es/>\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 2026.8.1\n" "X-Generator: Weblate 2026.6.1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "English" msgid "English"
@@ -93,7 +93,7 @@ msgstr "Hebreo"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Hungarian" msgid "Hungarian"
msgstr "Húngaro" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
@@ -439,8 +439,10 @@ msgid "You cannot exchange a medium for a medium."
msgstr "No se puede cambiar un medio por otro." msgstr "No se puede cambiar un medio por otro."
#: pretix/api/views/checkin.py #: pretix/api/views/checkin.py
#, fuzzy
#| msgid "Product does not support medium exchange."
msgid "You cannot simulate a medium exchange." msgid "You cannot simulate a medium exchange."
msgstr "No se puede simular un intercambio de medio." msgstr "Este producto no admite el cambio de medio."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
@@ -1399,8 +1401,10 @@ msgid "Membership type"
msgstr "Tipo de suscripción" msgstr "Tipo de suscripción"
#: pretix/base/exporters/customers.py #: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "Purchase time"
msgid "Purchase ticket" msgid "Purchase ticket"
msgstr "Comprar entrada" msgstr "Hora de compra"
#: pretix/base/exporters/customers.py pretix/base/exporters/orderlist.py #: pretix/base/exporters/customers.py pretix/base/exporters/orderlist.py
#: pretix/base/exporters/waitinglist.py pretix/base/forms/questions.py #: pretix/base/exporters/waitinglist.py pretix/base/forms/questions.py
@@ -1417,6 +1421,8 @@ msgid "Start date"
msgstr "Fecha de inicio" msgstr "Fecha de inicio"
#: pretix/base/exporters/customers.py #: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "Start time from"
msgid "Start time" msgid "Start time"
msgstr "Hora de inicio" msgstr "Hora de inicio"
@@ -1431,8 +1437,10 @@ msgid "End date"
msgstr "Fecha final" msgstr "Fecha final"
#: pretix/base/exporters/customers.py #: pretix/base/exporters/customers.py
#, fuzzy
#| msgid "End: %(time)s"
msgid "End time" msgid "End time"
msgstr "Hora de finalización" msgstr "Fin: %(time)s"
#: pretix/base/exporters/dekodi.py pretix/base/exporters/invoices.py #: pretix/base/exporters/dekodi.py pretix/base/exporters/invoices.py
msgctxt "export_category" msgctxt "export_category"
@@ -4679,13 +4687,16 @@ msgid "This event is remote or partially remote."
msgstr "Este evento es remoto o parcialmente remoto." msgstr "Este evento es remoto o parcialmente remoto."
#: pretix/base/models/event.py #: pretix/base/models/event.py
#, fuzzy
#| msgid ""
#| "This will be used to let users know if the event is in a different "
#| "timezone and let’s us calculate users’ local times."
msgid "" msgid ""
"This will be used to let users know if the event is in a different timezone, " "This will be used to let users know if the event is in a different timezone, "
"and to let us calculate the local time of a user." "and to let us calculate the local time of a user."
msgstr "" msgstr ""
"Esto servirá para informar a los usuarios de si el evento se celebra en una " "Esto se utilizará para que los usuarios sepan si el evento se celebra en una "
"zona horaria diferente y para que podamos calcular la hora local de cada " "zona horaria diferente y nos permite calcular la hora local de los usuarios."
"usuario."
#: pretix/base/models/event.py pretix/base/models/organizer.py #: pretix/base/models/event.py pretix/base/models/organizer.py
#: pretix/control/navigation.py #: pretix/control/navigation.py
@@ -5812,7 +5823,7 @@ msgstr "Código de país (ISO 3166-1 alfa-2)"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Asked on" msgid "Asked on"
msgstr "Preguntado el" msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
@@ -7512,6 +7523,9 @@ msgid "The payment for this invoice has already been received."
msgstr "El pago de esta factura ya se ha recibido." msgstr "El pago de esta factura ya se ha recibido."
#: pretix/base/payment.py #: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment is already being processed and can not be canceled any more."
msgid "" msgid ""
"This payment is already being processed and cannot be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "Este pago ya se está procesando y ya no se puede cancelar." msgstr "Este pago ya se está procesando y ya no se puede cancelar."
@@ -7638,12 +7652,15 @@ msgid "This gift card was used in the meantime. Please try again."
msgstr "Mientras tanto, esta tarjeta de regalo se utilizó. Inténtalo de nuevo." msgstr "Mientras tanto, esta tarjeta de regalo se utilizó. Inténtalo de nuevo."
#: pretix/base/payment.py #: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment provider does not exist or the respective plugin is disabled."
msgid "" msgid ""
"This payment provider exists for historical purposes only and is no longer " "This payment provider exists for historical purposes only and is no longer "
"usable." "usable."
msgstr "" msgstr ""
"Este proveedor de pagos se mantiene únicamente con fines históricos y ya no " "Este proveedor de pago no existe o el plugin correspondiente está "
"se puede utilizar." "desactivado."
#: pretix/base/pdf.py #: pretix/base/pdf.py
msgid "Ticket code (barcode content)" msgid "Ticket code (barcode content)"
@@ -8285,12 +8302,16 @@ msgid "Presale end"
msgstr "Fin de la preventa" msgstr "Fin de la preventa"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, fuzzy
#| msgid "Order email"
msgid "Order creation" msgid "Order creation"
msgstr "Creación de pedidos" msgstr "Correo electrónico del pedido"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry" msgid "Order expiry"
msgstr "Caducidad del pedido" msgstr "Pedido caducado"
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
@@ -8319,22 +8340,22 @@ msgstr "No fijado"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\"" msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr "Una fecha relativa no puede expresarse como «antes de» para «{}»" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\"" msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr "Una fecha relativa no puede expresarse como «después de» para «{}»" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\"" msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr "Un tiempo relativo no puede expresarse como «antes de» para «{}»" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\"" msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr "Un tiempo relativo no puede expresarse como «después de» para «{}»" msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
@@ -9576,12 +9597,16 @@ msgstr ""
"una tarjeta regalo." "una tarjeta regalo."
#: pretix/base/services/orders.py #: pretix/base/services/orders.py
#, fuzzy
#| msgid ""
#| "You cannot change the price of a position that has been used to issue a "
#| "gift card."
msgid "" msgid ""
"You cannot change the ticket secret of a position that has been used to " "You cannot change the ticket secret of a position that has been used to "
"issue a gift card." "issue a gift card."
msgstr "" msgstr ""
"No se puede modificar el código secreto de un ticket correspondiente a una " "No se puede cambiar el precio de una posición que se ha usado para entregar "
"posición que se haya utilizado para emitir una tarjeta regalo." "una tarjeta regalo."
#: pretix/base/services/orders.py #: pretix/base/services/orders.py
#, python-brace-format #, python-brace-format
@@ -10360,12 +10385,12 @@ msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "No dates match your criteria." msgid "No dates match your criteria."
msgstr "No hay fechas que se ajusten a tus criterios." msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgctxt "subevents" msgctxt "subevents"
msgid "Text for empty date results" msgid "Text for empty date results"
msgstr "Texto para los resultados con fechas vacías" msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgctxt "subevents" msgctxt "subevents"
@@ -10376,11 +10401,6 @@ msgid ""
"touch with you to arrange further dates. We do not recommend more than one " "touch with you to arrange further dates. We do not recommend more than one "
"or two sentences." "or two sentences."
msgstr "" msgstr ""
"Este texto aparecerá si el calendario o la lista de fechas están vacíos, por "
"ejemplo, porque un mes no contiene ninguna fecha o porque el filtro "
"seleccionado por el usuario no arroja ningún resultado. Puedes aprovecharlo "
"para indicar cómo ponerse en contacto contigo para concertar otras citas. No "
"recomendamos que superen una o dos frases."
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
@@ -14340,20 +14360,28 @@ msgstr ""
"año en el que se emite la tarjeta de regalo." "año en el que se emite la tarjeta de regalo."
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term" msgid "Payment term"
msgstr "Condiciones de pago" msgstr "Fecha de pago"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "(Same as above)"
msgid "same as above" msgid "same as above"
msgstr "igual que arriba" msgstr "(Lo mismo que arriba)"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days" msgid "different payment term in days"
msgstr "plazo de pago diferente en días" msgstr "Plazo de pago en días"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes" msgid "different payment term in minutes"
msgstr "plazo de pago diferente en minutos" msgstr "Plazo de pago en minutos"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
@@ -15108,7 +15136,7 @@ msgstr "Fecha final"
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
msgid "Start time from" msgid "Start time from"
msgstr "Hora de inicio a partir de" msgstr "Hora de inicio"
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
msgid "Start time until" msgid "Start time until"
@@ -15368,8 +15396,10 @@ msgid "Source"
msgstr "Fuente" msgstr "Fuente"
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#, fuzzy
#| msgid "All vouchers"
msgid "All sources" msgid "All sources"
msgstr "Todas las fuentes" msgstr "Todos los vales de compra"
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
msgid "Team actions" msgid "Team actions"
@@ -15380,12 +15410,16 @@ msgid "Customer actions"
msgstr "Acciones de los clientes" msgstr "Acciones de los clientes"
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#, fuzzy
#| msgid "Device status"
msgid "Device actions" msgid "Device actions"
msgstr "Acciones del dispositivo" msgstr "Estado de los dispositivos"
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
#, fuzzy
#| msgid "Order email"
msgid "User email" msgid "User email"
msgstr "Correo electrónico del usuario" msgstr "Correo electrónico del pedido"
#: pretix/control/forms/filter.py pretix/control/navigation.py #: pretix/control/forms/filter.py pretix/control/navigation.py
msgid "All users" msgid "All users"
@@ -16907,33 +16941,40 @@ msgid ""
"because at least one of the selected vouchers has already been redeemed " "because at least one of the selected vouchers has already been redeemed "
"%(max_redeemed)s times." "%(max_redeemed)s times."
msgstr "" msgstr ""
"No puedes reducir el número máximo de canjes a %(max_usages)s, ya que al "
"menos uno de los vales seleccionados ya se ha canjeado %(max_redeemed)s "
"veces."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
#, fuzzy
#| msgid ""
#| "You cannot create a voucher that blocks quota as the selected product or "
#| "quota is currently sold out or completely reserved."
msgid "" msgid ""
"You cannot create a voucher that allows selection of a quota but has no date " "You cannot create a voucher that allows selection of a quota but has no date "
"selected." "selected."
msgstr "" msgstr ""
"No se puede crear un comprobante que permita seleccionar una cuota pero en " "No se puede crear un vale de compra que bloquee la cuota ya que el producto "
"el que no se haya seleccionado ninguna fecha." "seleccionado o la cuota está agotada o completamente reservada."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
#, fuzzy
#| msgid "The selected product does not allow to select a seat."
msgid "The selected quota does not match the selected subevent." msgid "The selected quota does not match the selected subevent."
msgstr "La cuota seleccionada no coincide con el subevento seleccionado." msgstr "El producto seleccionado no permite seleccionar una butaca."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
#, fuzzy
#| msgid ""
#| "There is not enough quota available on quota \"{}\" to perform the "
#| "operation."
msgid "There is no sufficient quota available to perform this change." msgid "There is no sufficient quota available to perform this change."
msgstr "No hay cuota suficiente disponible para realizar este cambio." msgstr ""
"No hay suficiente cuota disponible en la cuota \"{}\" para realizar esta "
"operación."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
msgid "" msgid ""
"Changing the maximum number of usages in bulk is not supported if any of the " "Changing the maximum number of usages in bulk is not supported if any of the "
"selected vouchers is assigned a seat." "selected vouchers is assigned a seat."
msgstr "" msgstr ""
"No es posible modificar de forma masiva el número máximo de usos si a alguno "
"de los vales seleccionados se le ha asignado una plaza."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
msgctxt "subevent" msgctxt "subevent"
@@ -16941,24 +16982,18 @@ msgid ""
"Changing the date in bulk is not supported if any of the selected vouchers " "Changing the date in bulk is not supported if any of the selected vouchers "
"is assigned a seat." "is assigned a seat."
msgstr "" msgstr ""
"No es posible modificar la fecha de forma masiva si a alguno de los "
"comprobantes seleccionados se le ha asignado una plaza."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
msgid "" msgid ""
"Changing the product to a quota is not supported if any of the selected " "Changing the product to a quota is not supported if any of the selected "
"vouchers is assigned a seat." "vouchers is assigned a seat."
msgstr "" msgstr ""
"No es posible cambiar el producto a una cuota si a alguno de los vales "
"seleccionados se le ha asignado una plaza."
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
msgid "" msgid ""
"This change cannot be completed because not all assigned seats of the " "This change cannot be completed because not all assigned seats of the "
"vouchers are still available" "vouchers are still available"
msgstr "" msgstr ""
"No es posible completar este cambio porque no todos los asientos asignados "
"de los vales siguen estando disponibles"
#: pretix/control/forms/vouchers.py #: pretix/control/forms/vouchers.py
msgid "Codes" msgid "Codes"
@@ -20413,24 +20448,34 @@ msgstr ""
"¡Genial!" "¡Genial!"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
#, fuzzy
#| msgid "Your new SPF record could look like this:"
msgid "Your new DKIM record should be set up as a CNAME record like this:" msgid "Your new DKIM record should be set up as a CNAME record like this:"
msgstr "" msgstr "El nuevo registro SPF podría tener el siguiente aspecto:"
"El nuevo registro DKIM debe configurarse como un registro CNAME de la "
"siguiente manera:"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
#, fuzzy
#| msgid ""
#| "We found an SPF record on your domain that includes this system. Great!"
msgid "We found a DKIM record on your domain for this system. Great!" msgid "We found a DKIM record on your domain for this system. Great!"
msgstr "" msgstr ""
"Hemos encontrado un registro DKIM en tu dominio para este sistema. ¡Genial!" "Hemos encontrado un registro SPF en su dominio que incluye este sistema. "
"¡Genial!"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
#, fuzzy
#| msgid "Your new SPF record could look like this:"
msgid "Your new DMARC record could look like this:" msgid "Your new DMARC record could look like this:"
msgstr "El nuevo registro DMARC podría tener este aspecto:" msgstr "El nuevo registro SPF podría tener el siguiente aspecto:"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
#, fuzzy
#| msgid ""
#| "We found an SPF record on your domain that includes this system. Great!"
msgid "We found a DMARC record on your domain for this system. Great!" msgid "We found a DMARC record on your domain for this system. Great!"
msgstr "" msgstr ""
"Hemos encontrado un registro DMARC en tu dominio para este sistema. ¡Genial!" "Hemos encontrado un registro SPF en su dominio que incluye este sistema. "
"¡Genial!"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
msgid "Verification" msgid "Verification"
@@ -22141,8 +22186,10 @@ msgid "The quick brown fox jumps over the lazy dog."
msgstr "El veloz zorro marrón salta sobre el perro perezoso." msgstr "El veloz zorro marrón salta sobre el perro perezoso."
#: pretix/control/templates/pretixcontrol/fragment_log_filter_form.html #: pretix/control/templates/pretixcontrol/fragment_log_filter_form.html
#, fuzzy
#| msgid "Specific seat"
msgid "Specific object selected" msgid "Specific object selected"
msgstr "Se ha seleccionado un objeto concreto" msgstr "Butaca especifica"
#: pretix/control/templates/pretixcontrol/fragment_quota_box.html #: pretix/control/templates/pretixcontrol/fragment_quota_box.html
#: pretix/control/templates/pretixcontrol/fragment_quota_box_paid.html #: pretix/control/templates/pretixcontrol/fragment_quota_box_paid.html
@@ -23215,24 +23262,28 @@ msgstr ""
"sus usuarios acerca de las necesidades dietéticas." "sus usuarios acerca de las necesidades dietéticas."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new per-ticket question" msgid "Create a new per-ticket question"
msgstr "Crear una nueva pregunta por ticket" msgstr "Crear una nueva pregunta"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new order-level question" msgid "Create a new order-level question"
msgstr "Crear una nueva pregunta a nivel de pedido" msgstr "Crear una nueva pregunta"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions" msgid "Per-ticket questions"
msgstr "Preguntas por entrada" msgstr "Borrar pregunta"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"These questions are asked for every ticket, so possibly multiple times in " "These questions are asked for every ticket, so possibly multiple times in "
"the same order." "the same order."
msgstr "" msgstr ""
"Estas preguntas se formulan para cada entrada, por lo que es posible que se "
"repitan varias veces en el mismo pedido."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
@@ -23251,28 +23302,28 @@ msgid "All personalized products"
msgstr "Todos los productos personalizados" msgstr "Todos los productos personalizados"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Include questions"
msgid "Per-order questions" msgid "Per-order questions"
msgstr "Preguntas por pedido" msgstr "Incluir preguntas"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"This functionality is in active development and expected to change " "This functionality is in active development and expected to change "
"significantly over the coming months." "significantly over the coming months."
msgstr "" msgstr ""
"Esta funcionalidad se encuentra en fase de desarrollo activo y se prevé que "
"sufra cambios significativos en los próximos meses."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"Per-order questions are currently not supported and will not be displayed in " "Per-order questions are currently not supported and will not be displayed in "
"pretixPOS." "pretixPOS."
msgstr "" msgstr ""
"Actualmente no se admiten las preguntas por pedido y no se mostrarán en "
"pretixPOS."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "The question has been reordered."
msgid "These questions are asked once per order." msgid "These questions are asked once per order."
msgstr "Estas preguntas se formulan una vez por pedido." msgstr "La pregunta ha sido reordenada."
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
@@ -23804,8 +23855,6 @@ msgid ""
"Ticket secrets of order positions that have been used to issue a gift card " "Ticket secrets of order positions that have been used to issue a gift card "
"can not be changed. Only the link will be changed in this case." "can not be changed. Only the link will be changed in this case."
msgstr "" msgstr ""
"Los datos de los pedidos que se han utilizado para emitir una tarjeta regalo "
"no se pueden modificar. En este caso, solo se modificará el enlace."
#: pretix/control/templates/pretixcontrol/order/change.html #: pretix/control/templates/pretixcontrol/order/change.html
msgid "" msgid ""
@@ -23881,8 +23930,10 @@ msgstr "(opcional)"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html #: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html #: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Additional information"
msgid "Additional order information" msgid "Additional order information"
msgstr "Información adicional sobre el pedido" msgstr "Información adicional"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
@@ -25584,13 +25635,14 @@ msgid "Hardware model"
msgstr "Modelo del Hardware" msgstr "Modelo del Hardware"
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format #, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s" msgid "Last seen: %(time)s"
msgstr "Última conexión: %(time)s" msgstr "Inicio: %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact" msgid "No recent contact"
msgstr "No ha habido contacto reciente" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
@@ -27939,8 +27991,10 @@ msgstr ""
"producto!" "producto!"
#: pretix/control/templates/pretixcontrol/vouchers/bulk_edit.html #: pretix/control/templates/pretixcontrol/vouchers/bulk_edit.html
#, fuzzy
#| msgid "Create multiple vouchers"
msgid "Change multiple vouchers" msgid "Change multiple vouchers"
msgstr "Modificar varios vales" msgstr "Crear múltiples vales de compra"
#: pretix/control/templates/pretixcontrol/vouchers/delete.html #: pretix/control/templates/pretixcontrol/vouchers/delete.html
#: pretix/control/templates/pretixcontrol/vouchers/detail.html #: pretix/control/templates/pretixcontrol/vouchers/detail.html
@@ -28351,9 +28405,6 @@ msgid ""
"team. If you want to add a different user or create a new account, log out " "team. If you want to add a different user or create a new account, log out "
"and click the invitation link again." "and click the invitation link again."
msgstr "" msgstr ""
"No puedes aceptar la invitación para «{}», ya que ya formas parte de este "
"equipo. Si quieres añadir a otro usuario o crear una nueva cuenta, cierra la "
"sesión y vuelve a hacer clic en el enlace de invitación."
#: pretix/control/views/auth.py #: pretix/control/views/auth.py
#, python-brace-format #, python-brace-format
@@ -29037,6 +29088,13 @@ msgstr ""
"SPF." "SPF."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We could not find an SPF record set for the domain you are trying to use. "
#| "This means that there is a very high change most of the emails will be "
#| "rejected or marked as spam. We strongly recommend setting an SPF record "
#| "on the domain. You can do so through the DNS settings at the provider you "
#| "registered your domain with."
msgid "" msgid ""
"We could not find a CNAME record pointing to our DKIM key for domain you are " "We could not find a CNAME record pointing to our DKIM key for domain you are "
"trying to use. This means that there is a very high change most of the " "trying to use. This means that there is a very high change most of the "
@@ -29044,35 +29102,53 @@ msgid ""
"DKIM through a CNAME record. You can do so through the DNS settings at the " "DKIM through a CNAME record. You can do so through the DNS settings at the "
"provider you registered your domain with." "provider you registered your domain with."
msgstr "" msgstr ""
"No hemos podido encontrar un registro CNAME que apunte a nuestra clave DKIM " "No se pudo encontrar un registro SPF configurado para el dominio que está "
"para el dominio que estás intentando utilizar. Esto significa que hay una " "intentando usar. Esto significa que existe una alta probabilidad de que la "
"probabilidad muy alta de que la mayoría de los correos electrónicos sean " "mayoría de los correos electrónicos sean rechazados o marcados como spam. "
"rechazados o marcados como spam. Te recomendamos encarecidamente que " "Recomendamos encarecidamente configurar un registro SPF en el dominio. Puede "
"configures DKIM mediante un registro CNAME. Puedes hacerlo a través de la " "hacerlo a través de la configuración de DNS en el proveedor con el que "
"configuración de DNS del proveedor con el que registraste tu dominio." "registró su dominio."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We found an SPF record set for the domain you are trying to use, but it "
#| "does not include this system's email server. This means that there is a "
#| "very high chance most of the emails will be rejected or marked as spam. "
#| "You should update the DNS settings of your domain to include this system "
#| "in the SPF record."
msgid "" msgid ""
"We found a CNAME record for a DKIM key, but it is not pointing to the right " "We found a CNAME record for a DKIM key, but it is not pointing to the right "
"location. This means that there is a very high chance most of the emails " "location. This means that there is a very high chance most of the emails "
"will be rejected or marked as spam. You should update the DNS settings of " "will be rejected or marked as spam. You should update the DNS settings of "
"your domain." "your domain."
msgstr "" msgstr ""
"Hemos encontrado un registro CNAME para una clave DKIM, pero no apunta a la " "Hemos encontrado un registro SPF configurado para el dominio que está "
"ubicación correcta. Esto significa que hay muchas posibilidades de que la " "intentando utilizar, pero no incluye el servidor de correo electrónico del "
"mayoría de los correos electrónicos sean rechazados o marcados como spam. " "mismo sistema. Esto significa que es muy probable que la mayoría de los "
"Deberías actualizar la configuración DNS de tu dominio." "correos electrónicos sean rechazados o marcados como spam. Debe actualizar "
"la configuración DNS de su dominio para incluir este sistema en el registro "
"SPF."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We found an SPF record set for the domain you are trying to use, but it "
#| "does not include this system's email server. This means that there is a "
#| "very high chance most of the emails will be rejected or marked as spam. "
#| "You should update the DNS settings of your domain to include this system "
#| "in the SPF record."
msgid "" msgid ""
"We did not find a DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
"No hemos encontrado ningún registro DMARC para tu dominio. Esto significa " "Hemos encontrado un registro SPF configurado para el dominio que está "
"que hay muchas posibilidades de que la mayoría de los correos electrónicos " "intentando utilizar, pero no incluye el servidor de correo electrónico del "
"sean rechazados o marcados como spam. Deberías actualizar la configuración " "mismo sistema. Esto significa que es muy probable que la mayoría de los "
"DNS de tu dominio." "correos electrónicos sean rechazados o marcados como spam. Debe actualizar "
"la configuración DNS de su dominio para incluir este sistema en el registro "
"SPF."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "The verification code was incorrect, please try again." msgid "The verification code was incorrect, please try again."
@@ -31825,7 +31901,6 @@ msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold" msgid "Allow further payments during compliance hold"
msgstr "" msgstr ""
"Permitir que se realicen más pagos durante la suspensión por incumplimiento"
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "" msgid ""
@@ -31835,24 +31910,16 @@ msgid ""
"attempts during that window. This might result in them being charged twice " "attempts during that window. This might result in them being charged twice "
"if the original payment is approved." "if the original payment is approved."
msgstr "" msgstr ""
"El sistema de prevención de fraudes de PayPal podría bloquear la tramitación "
"de pagos individuales durante un periodo de tiempo considerable. Durante ese "
"intervalo, el pago aparece como «pendiente». Puedes permitir que tus "
"clientes realicen nuevos intentos de pago durante ese intervalo. Esto podría "
"dar lugar a que se les cobre dos veces si se aprueba el pago original."
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts" msgid "Timeout further payment attempts"
msgstr "Tiempo de espera para nuevos intentos de pago" msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "" msgid ""
"Time duration in minutes after which another payment attempt is possible, " "Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation." "while the last payment is still under investigation."
msgstr "" msgstr ""
"Tiempo, expresado en minutos, transcurrido tras el cual es posible realizar "
"otro intento de pago, mientras el último pago sigue siendo objeto de "
"investigación."
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
@@ -32121,11 +32188,6 @@ msgid ""
"twice in case PayPal allows your initial payment attempt. Please contact us " "twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case." "to resolve this case."
msgstr "" msgstr ""
"PayPal está procesando tu pago. Este proceso está tardando más de lo "
"habitual. Puedes esperar a que PayPal confirme el pago o intentar volver a "
"pagar con este u otro método de pago. Esto podría dar lugar a que se te "
"cobre dos veces en caso de que PayPal acepte tu primer intento de pago. "
"Ponte en contacto con nosotros para resolver este asunto."
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
msgid "" msgid ""
@@ -32377,21 +32439,24 @@ msgid "Base redirection URLs"
msgstr "URL de redirección de base" msgstr "URL de redirección de base"
#: pretix/plugins/returnurl/views.py #: pretix/plugins/returnurl/views.py
#, fuzzy
#| msgid ""
#| "Redirection will only be allowed to URLs that start with one of these "
#| "prefixes. Enter one or more allowed URL prefix per line. URL prefixes "
#| "must include a slash after the hostname."
msgid "" msgid ""
"Redirection will only be allowed to URLs that start with one of these " "Redirection will only be allowed to URLs that start with one of these "
"prefixes. Enter one allowed URL prefix per line. URL prefixes must include a " "prefixes. Enter one allowed URL prefix per line. URL prefixes must include a "
"slash after the hostname." "slash after the hostname."
msgstr "" msgstr ""
"Solo se permitirá la redirección a direcciones URL que empiecen por uno de " "La redirección sólo se permitirá a las URL que empiecen por uno de estos "
"estos prefijos. Introduce un prefijo de URL permitido por línea. Los " "prefijos. Introduzca uno o más prefijos de URL permitidos por línea. Los "
"prefijos de URL deben incluir una barra después del nombre de host." "prefijos de URL deben incluir una barra después del nombre de host."
#: pretix/plugins/returnurl/views.py #: pretix/plugins/returnurl/views.py
msgid "" msgid ""
"All values must be URLs that include at last one slash after the hostname." "All values must be URLs that include at last one slash after the hostname."
msgstr "" msgstr ""
"Todos los valores deben ser direcciones URL que incluyan al menos una barra "
"después del nombre de host."
#: pretix/plugins/sendmail/apps.py #: pretix/plugins/sendmail/apps.py
msgid "Send out emails to all your customers or specific groups of customers." msgid "Send out emails to all your customers or specific groups of customers."
+7 -7
View File
@@ -8,16 +8,16 @@ 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: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-04 14:29+0000\n" "PO-Revision-Date: 2026-08-24 14:30+0000\n"
"Last-Translator: Albizuri <oier@puntu.eus>\n" "Last-Translator: Albizuri <oier@puntu.eus>\n"
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/eu/"
"eu/>\n" ">\n"
"Language: eu\n" "Language: eu\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"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 2026.9\n" "X-Generator: Weblate 2026.8.1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "English" msgid "English"
@@ -891,7 +891,7 @@ msgstr "Produktuaren data"
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/presale/templates/pretixpresale/event/order.html #: pretix/presale/templates/pretixpresale/event/order.html
msgid "Order details" msgid "Order details"
msgstr "Eskaeraren xehetasunak" msgstr ""
#: pretix/base/datasync/sourcefields.py pretix/base/modelimport_orders.py #: pretix/base/datasync/sourcefields.py pretix/base/modelimport_orders.py
#: pretix/control/forms/filter.py #: pretix/control/forms/filter.py
@@ -17296,7 +17296,7 @@ msgstr ""
#: pretix/control/logdisplay.py #: pretix/control/logdisplay.py
msgid "The order details have been changed." msgid "The order details have been changed."
msgstr "Eskaeraren xehetasunak aldatu dira." msgstr ""
#: pretix/control/logdisplay.py #: pretix/control/logdisplay.py
msgid "The order has been marked as unpaid." msgid "The order has been marked as unpaid."
@@ -23106,7 +23106,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#, python-format #, python-format
msgid "Order details: %(code)s" msgid "Order details: %(code)s"
msgstr "Eskaeraren xehetasunak: %(code)s" msgstr ""
#: pretix/control/templates/pretixcontrol/order/index.html #: pretix/control/templates/pretixcontrol/order/index.html
#: pretix/control/templates/pretixcontrol/orders/index.html #: pretix/control/templates/pretixcontrol/orders/index.html
+110 -77
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n" "Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-08-25 00:00+0000\n" "PO-Revision-Date: 2026-08-11 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n" "Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
"fr/>\n" ">\n"
"Language: fr\n" "Language: fr\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -89,7 +89,7 @@ msgstr "Hébreu"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Hungarian" msgid "Hungarian"
msgstr "Hongrois" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
@@ -438,8 +438,10 @@ msgid "You cannot exchange a medium for a medium."
msgstr "Il n'est pas possible d'échanger un support contre un autre support." msgstr "Il n'est pas possible d'échanger un support contre un autre support."
#: pretix/api/views/checkin.py #: pretix/api/views/checkin.py
#, fuzzy
#| msgid "Product does not support medium exchange."
msgid "You cannot simulate a medium exchange." msgid "You cannot simulate a medium exchange."
msgstr "l n'est pas possible de simuler un échange de support." msgstr "Ce produit ne permet pas de changer de support."
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
@@ -5831,7 +5833,7 @@ msgstr "Code pays (ISO 3166-1 alpha-2)"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Asked on" msgid "Asked on"
msgstr "Question posée le" msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
@@ -7553,6 +7555,9 @@ msgid "The payment for this invoice has already been received."
msgstr "Le paiement de cette facture a déjà été reçu." msgstr "Le paiement de cette facture a déjà été reçu."
#: pretix/base/payment.py #: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment is already being processed and can not be canceled any more."
msgid "" msgid ""
"This payment is already being processed and cannot be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "" msgstr ""
@@ -8335,12 +8340,16 @@ msgid "Presale end"
msgstr "Fin de la prévente" msgstr "Fin de la prévente"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, fuzzy
#| msgid "Order email"
msgid "Order creation" msgid "Order creation"
msgstr "Création d'une commande" msgstr "E-mail de la commande"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry" msgid "Order expiry"
msgstr "Expiration de la commande" msgstr "Commande expirée"
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
@@ -8369,22 +8378,22 @@ msgstr "Non réglé"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\"" msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr "Une date relative ne peut pas être exprimée par « avant » pour « {} »" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\"" msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr "Une date relative ne peut pas être exprimée par « après » pour « {} »" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\"" msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr "Une durée relative ne peut pas être exprimée par « avant » pour « {} »" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\"" msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr "Un temps relatif ne peut pas être exprimé par « après » pour « {} »" msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
@@ -10421,12 +10430,12 @@ msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "No dates match your criteria." msgid "No dates match your criteria."
msgstr "Aucune date ne correspond à vos critères." msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgctxt "subevents" msgctxt "subevents"
msgid "Text for empty date results" msgid "Text for empty date results"
msgstr "Texte à afficher lorsque les résultats ne contiennent aucune date" msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgctxt "subevents" msgctxt "subevents"
@@ -10437,11 +10446,6 @@ msgid ""
"touch with you to arrange further dates. We do not recommend more than one " "touch with you to arrange further dates. We do not recommend more than one "
"or two sentences." "or two sentences."
msgstr "" msgstr ""
"Ce texte s'affichera si le calendrier ou la liste des dates est vide, par "
"exemple parce qu'un mois ne comporte aucune date ou qu'un filtre sélectionné "
"par l'utilisateur ne donne aucun résultat. Vous pouvez en profiter pour "
"indiquer comment vous contacter afin de convenir d'autres dates. Nous vous "
"recommandons de ne pas dépasser une ou deux phrases."
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
@@ -14475,20 +14479,28 @@ msgstr ""
"plus l’année d’émission de la carte-cadeau." "plus l’année d’émission de la carte-cadeau."
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term" msgid "Payment term"
msgstr "Conditions de paiement" msgstr "Date de paiement"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "(Same as above)"
msgid "same as above" msgid "same as above"
msgstr "idem que ci-dessus" msgstr "(identique à ce qui précède)"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days" msgid "different payment term in days"
msgstr "délai de paiement différent en jours" msgstr "Délai de paiement en jours"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes" msgid "different payment term in minutes"
msgstr "durée de paiement différente en minutes" msgstr "Délai de paiement en minutes"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
@@ -20565,20 +20577,26 @@ msgstr ""
"CNAME comme ceci :" "CNAME comme ceci :"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
#, fuzzy
#| msgid ""
#| "We found an SPF record on your domain that includes this system. Great!"
msgid "We found a DKIM record on your domain for this system. Great!" msgid "We found a DKIM record on your domain for this system. Great!"
msgstr "" msgstr ""
"Nous avons trouvé un enregistrement DKIM sur votre domaine pour ce système. " "Nous avons trouvé un enregistrement SPF sur votre domaine qui inclut ce "
"Parfait !" "système. Super !"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
msgid "Your new DMARC record could look like this:" msgid "Your new DMARC record could look like this:"
msgstr "Votre nouvel enregistrement DMARC pourrait ressembler à ceci :" msgstr "Votre nouvel enregistrement DMARC pourrait ressembler à ceci :"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
#, fuzzy
#| msgid ""
#| "We found an SPF record on your domain that includes this system. Great!"
msgid "We found a DMARC record on your domain for this system. Great!" msgid "We found a DMARC record on your domain for this system. Great!"
msgstr "" msgstr ""
"Nous avons trouvé un enregistrement DMARC sur votre domaine pour ce système. " "Nous avons trouvé un enregistrement SPF sur votre domaine qui inclut ce "
"Parfait !" "système. Super !"
#: pretix/control/templates/pretixcontrol/email_setup_simple.html #: pretix/control/templates/pretixcontrol/email_setup_simple.html
msgid "Verification" msgid "Verification"
@@ -23389,24 +23407,28 @@ msgstr ""
"besoins alimentaires." "besoins alimentaires."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new per-ticket question" msgid "Create a new per-ticket question"
msgstr "Créer une nouvelle question spécifique à chaque ticket" msgstr "Créer une nouvelle question"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new order-level question" msgid "Create a new order-level question"
msgstr "Créer une nouvelle question au niveau de la commande" msgstr "Créer une nouvelle question"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions" msgid "Per-ticket questions"
msgstr "Questions relatives à chaque billet" msgstr "Supprimer la question"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"These questions are asked for every ticket, so possibly multiple times in " "These questions are asked for every ticket, so possibly multiple times in "
"the same order." "the same order."
msgstr "" msgstr ""
"Ces questions sont posées pour chaque billet, et peuvent donc être posées "
"plusieurs fois au cours d'une même commande."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
@@ -23425,28 +23447,28 @@ msgid "All personalized products"
msgstr "Tous les produits personnalisés" msgstr "Tous les produits personnalisés"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Include questions"
msgid "Per-order questions" msgid "Per-order questions"
msgstr "Questions relatives à chaque commande" msgstr "Inclure des questions"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"This functionality is in active development and expected to change " "This functionality is in active development and expected to change "
"significantly over the coming months." "significantly over the coming months."
msgstr "" msgstr ""
"Cette fonctionnalité est en cours de développement et devrait évoluer "
"considérablement au cours des prochains mois."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"Per-order questions are currently not supported and will not be displayed in " "Per-order questions are currently not supported and will not be displayed in "
"pretixPOS." "pretixPOS."
msgstr "" msgstr ""
"Les questions spécifiques à chaque commande ne sont actuellement pas prises "
"en charge et n'apparaîtront pas dans pretixPOS."
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "The question has been reordered."
msgid "These questions are asked once per order." msgid "These questions are asked once per order."
msgstr "Ces questions sont posées une fois par commande." msgstr "La question a été réordonnée."
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
@@ -24058,8 +24080,10 @@ msgstr "(optionnel)"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html #: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html #: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Additional information"
msgid "Additional order information" msgid "Additional order information"
msgstr "Informations complémentaires sur la commande" msgstr "Informations complémentaires"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
@@ -25775,13 +25799,14 @@ msgid "Hardware model"
msgstr "Modèle de matériel" msgstr "Modèle de matériel"
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format #, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s" msgid "Last seen: %(time)s"
msgstr "Dernière connexion : %(time)s" msgstr "Début : %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact" msgid "No recent contact"
msgstr "Aucun contact récent" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
@@ -29261,6 +29286,13 @@ msgstr ""
"l'enregistrement SPF." "l'enregistrement SPF."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We could not find an SPF record set for the domain you are trying to use. "
#| "This means that there is a very high change most of the emails will be "
#| "rejected or marked as spam. We strongly recommend setting an SPF record "
#| "on the domain. You can do so through the DNS settings at the provider you "
#| "registered your domain with."
msgid "" msgid ""
"We could not find a CNAME record pointing to our DKIM key for domain you are " "We could not find a CNAME record pointing to our DKIM key for domain you are "
"trying to use. This means that there is a very high change most of the " "trying to use. This means that there is a very high change most of the "
@@ -29268,35 +29300,53 @@ msgid ""
"DKIM through a CNAME record. You can do so through the DNS settings at the " "DKIM through a CNAME record. You can do so through the DNS settings at the "
"provider you registered your domain with." "provider you registered your domain with."
msgstr "" msgstr ""
"Nous n'avons pas trouvé d'enregistrement CNAME pointant vers notre clé DKIM " "Nous n'avons pas trouvé d'enregistrement SPF pour le domaine que vous "
"pour le domaine que vous essayez d'utiliser. Cela signifie qu'il y a de très " "essayez d'utiliser. Cela signifie qu'il y a de fortes chances que la plupart "
"fortes chances que la plupart des e-mails soient rejetés ou marqués comme " "des e-mails soient rejetés ou marqués comme spam. Nous vous recommandons "
"spam. Nous vous recommandons vivement de configurer DKIM à l'aide d'un " "vivement de configurer un enregistrement SPF sur le domaine. Vous pouvez le "
"enregistrement CNAME. Vous pouvez le faire via les paramètres DNS chez le " "faire via les paramètres DNS chez le fournisseur auprès duquel vous avez "
"fournisseur auprès duquel vous avez enregistré votre domaine." "enregistré votre domaine."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We found an SPF record set for the domain you are trying to use, but it "
#| "does not include this system's email server. This means that there is a "
#| "very high chance most of the emails will be rejected or marked as spam. "
#| "You should update the DNS settings of your domain to include this system "
#| "in the SPF record."
msgid "" msgid ""
"We found a CNAME record for a DKIM key, but it is not pointing to the right " "We found a CNAME record for a DKIM key, but it is not pointing to the right "
"location. This means that there is a very high chance most of the emails " "location. This means that there is a very high chance most of the emails "
"will be rejected or marked as spam. You should update the DNS settings of " "will be rejected or marked as spam. You should update the DNS settings of "
"your domain." "your domain."
msgstr "" msgstr ""
"Nous avons détecté un enregistrement CNAME associé à une clé DKIM, mais " "Nous avons trouvé un enregistrement SPF défini pour le domaine que vous "
"celui-ci ne pointe pas vers la bonne adresse. Cela signifie qu'il y a de " "essayez d'utiliser, mais il n'inclut pas le serveur de messagerie de ce "
"très fortes chances que la plupart de vos e-mails soient rejetés ou classés " "système. Cela signifie qu'il y a de fortes chances que la plupart des e-"
"comme spam. Vous devez mettre à jour les paramètres DNS de votre domaine." "mails soient rejetés ou marqués comme spam. Vous devez mettre à jour les "
"paramètres DNS de votre domaine afin d'inclure ce système dans "
"l'enregistrement SPF."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We found an SPF record set for the domain you are trying to use, but it "
#| "does not include this system's email server. This means that there is a "
#| "very high chance most of the emails will be rejected or marked as spam. "
#| "You should update the DNS settings of your domain to include this system "
#| "in the SPF record."
msgid "" msgid ""
"We did not find a DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
"Nous n'avons pas trouvé d'enregistrement DMARC pour votre domaine. Cela " "Nous avons trouvé un enregistrement SPF défini pour le domaine que vous "
"signifie qu'il y a de très fortes chances que la plupart de vos e-mails " "essayez d'utiliser, mais il n'inclut pas le serveur de messagerie de ce "
"soient rejetés ou marqués comme spam. Vous devriez mettre à jour les " "système. Cela signifie qu'il y a de fortes chances que la plupart des e-"
"paramètres DNS de votre domaine." "mails soient rejetés ou marqués comme spam. Vous devez mettre à jour les "
"paramètres DNS de votre domaine afin d'inclure ce système dans "
"l'enregistrement SPF."
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "The verification code was incorrect, please try again." msgid "The verification code was incorrect, please try again."
@@ -31982,8 +32032,8 @@ msgid ""
"We're waiting for an answer from PayPal regarding your payment. Please " "We're waiting for an answer from PayPal regarding your payment. Please "
"contact us, if this takes more than a few hours." "contact us, if this takes more than a few hours."
msgstr "" msgstr ""
"Nous attendons une réponse de PayPal concernant votre paiement. N'hésitez " "Nous attendons une réponse de PayPal concernant votre paiement. Veuillez "
"pas à nous contacter si cela prend plus de quelques heures." "nous contacter, si cela prend plus de quelques heures."
#: pretix/plugins/paypal/views.py pretix/plugins/paypal2/views.py #: pretix/plugins/paypal/views.py pretix/plugins/paypal2/views.py
msgid "Invalid response from PayPal received." msgid "Invalid response from PayPal received."
@@ -32081,8 +32131,6 @@ msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold" msgid "Allow further payments during compliance hold"
msgstr "" msgstr ""
"Autoriser la poursuite des paiements pendant la période de suspension pour "
"non-conformité"
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "" msgid ""
@@ -32092,25 +32140,16 @@ msgid ""
"attempts during that window. This might result in them being charged twice " "attempts during that window. This might result in them being charged twice "
"if the original payment is approved." "if the original payment is approved."
msgstr "" msgstr ""
"Le système de prévention des fraudes de PayPal peut bloquer le traitement de "
"certains paiements pendant une durée considérable. Pendant cette période, le "
"paiement est marqué comme « en attente ». Vous pouvez autoriser vos clients "
"à effectuer de nouvelles tentatives de paiement pendant cette période. Cela "
"peut entraîner un double prélèvement si le paiement initial est finalement "
"approuvé."
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts" msgid "Timeout further payment attempts"
msgstr "Délai d'expiration des tentatives de paiement supplémentaires" msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "" msgid ""
"Time duration in minutes after which another payment attempt is possible, " "Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation." "while the last payment is still under investigation."
msgstr "" msgstr ""
"Durée en minutes à l'issue de laquelle une nouvelle tentative de paiement "
"est possible, alors que le dernier paiement fait toujours l'objet d'une "
"enquête."
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
@@ -32388,12 +32427,6 @@ msgid ""
"twice in case PayPal allows your initial payment attempt. Please contact us " "twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case." "to resolve this case."
msgstr "" msgstr ""
"Votre paiement est en cours de traitement par PayPal. Cette opération prend "
"plus de temps que d'habitude. Vous pouvez attendre que PayPal valide le "
"paiement ou essayer de payer à nouveau en utilisant ce moyen de paiement ou "
"un autre. Cela pourrait entraîner un double prélèvement si PayPal autorise "
"votre première tentative de paiement. Veuillez nous contacter pour résoudre "
"ce problème."
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
msgid "" msgid ""
+70 -51
View File
@@ -8,8 +8,8 @@ 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: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-07 18:00+0000\n" "PO-Revision-Date: 2026-08-16 22:00+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n" "Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n" "ja/>\n"
"Language: ja\n" "Language: ja\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=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 2026.9\n" "X-Generator: Weblate 2026.8.1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "English" msgid "English"
@@ -93,7 +93,7 @@ msgstr "ヘブライ語"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Hungarian" msgid "Hungarian"
msgstr "ハンガリー語" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
@@ -220,7 +220,7 @@ msgstr "ターゲットURL"
#: pretix/api/models.py pretix/base/models/devices.py #: pretix/api/models.py pretix/base/models/devices.py
#: pretix/base/models/organizer.py #: pretix/base/models/organizer.py
msgid "All events (including newly created ones)" msgid "All events (including newly created ones)"
msgstr "すべてのイベント(今後作成されるものを含む)" msgstr "すべてのイベント(最近作成されたイベントを含む)"
#: pretix/api/models.py pretix/base/models/devices.py #: pretix/api/models.py pretix/base/models/devices.py
#: pretix/base/models/organizer.py #: pretix/base/models/organizer.py
@@ -428,8 +428,10 @@ msgid "You cannot exchange a medium for a medium."
msgstr "メディアを別のメディアに変更することはできません。" msgstr "メディアを別のメディアに変更することはできません。"
#: pretix/api/views/checkin.py #: pretix/api/views/checkin.py
#, fuzzy
#| msgid "Product does not support medium exchange."
msgid "You cannot simulate a medium exchange." msgid "You cannot simulate a medium exchange."
msgstr "メディアの交換をシミュレートすることはできません。" msgstr "本製品は中程度の交換に対応していません。"
#: pretix/api/views/oauth.py pretix/control/logdisplay.py #: pretix/api/views/oauth.py pretix/control/logdisplay.py
#, python-brace-format #, python-brace-format
@@ -5689,7 +5691,7 @@ msgstr "国コード(ISO 3166-1 alpha-2)"
#: pretix/base/models/items.py #: pretix/base/models/items.py
msgid "Asked on" msgid "Asked on"
msgstr "質問日" msgstr ""
#: pretix/base/models/items.py pretix/base/models/organizer.py #: pretix/base/models/items.py pretix/base/models/organizer.py
msgid "" msgid ""
@@ -7338,9 +7340,12 @@ msgid "The payment for this invoice has already been received."
msgstr "この請求書の支払いはすでに受領済みです。" msgstr "この請求書の支払いはすでに受領済みです。"
#: pretix/base/payment.py #: pretix/base/payment.py
#, fuzzy
#| msgid ""
#| "This payment is already being processed and can not be canceled any more."
msgid "" msgid ""
"This payment is already being processed and cannot be canceled any more." "This payment is already being processed and cannot be canceled any more."
msgstr "この支払いはすでに処理中のため、キャンセルできません。" msgstr "この支払いは処理中のため、キャンセルできません。"
#: pretix/base/payment.py #: pretix/base/payment.py
msgid "Automatic refunds are not supported by this payment provider." msgid "Automatic refunds are not supported by this payment provider."
@@ -8100,12 +8105,16 @@ msgid "Presale end"
msgstr "前売り終了" msgstr "前売り終了"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, fuzzy
#| msgid "Order email"
msgid "Order creation" msgid "Order creation"
msgstr "注文の作成" msgstr "注文者メール"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, fuzzy
#| msgid "Order expired"
msgid "Order expiry" msgid "Order expiry"
msgstr "注文の失効" msgstr "注文の有効期限が切れました"
#: pretix/base/reldate.py #: pretix/base/reldate.py
msgid "before" msgid "before"
@@ -8134,22 +8143,22 @@ msgstr "未設定"
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative date cannot be expressed as \"before\" for \"{}\"" msgid "A relative date cannot be expressed as \"before\" for \"{}\""
msgstr "相対的な日付は、「{}」に対して「before」で表すことはできません" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative date cannot be expressed as \"after\" for \"{}\"" msgid "A relative date cannot be expressed as \"after\" for \"{}\""
msgstr "相対的な日付は「{}」に対して「after」として表すことはできません" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative time cannot be expressed as \"before\" for \"{}\"" msgid "A relative time cannot be expressed as \"before\" for \"{}\""
msgstr "相対時間は「{}」に対して「before」で表すことはできません" msgstr ""
#: pretix/base/reldate.py #: pretix/base/reldate.py
#, python-brace-format #, python-brace-format
msgid "A relative time cannot be expressed as \"after\" for \"{}\"" msgid "A relative time cannot be expressed as \"after\" for \"{}\""
msgstr "相対時間は「{}」に対して「after」で表すことはできません" msgstr ""
#: pretix/base/secrets.py #: pretix/base/secrets.py
msgid "Random (default, works with all pretix apps)" msgid "Random (default, works with all pretix apps)"
@@ -10058,12 +10067,12 @@ msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "No dates match your criteria." msgid "No dates match your criteria."
msgstr "条件に合致する日付はありません。" msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgctxt "subevents" msgctxt "subevents"
msgid "Text for empty date results" msgid "Text for empty date results"
msgstr "空の日付結果のテキスト" msgstr ""
#: pretix/base/settings.py #: pretix/base/settings.py
msgctxt "subevents" msgctxt "subevents"
@@ -10074,10 +10083,6 @@ msgid ""
"touch with you to arrange further dates. We do not recommend more than one " "touch with you to arrange further dates. We do not recommend more than one "
"or two sentences." "or two sentences."
msgstr "" msgstr ""
"このテキストは、カレンダーまたは日付リストが空の場合に表示されます。たとえば"
"、月に日付が含まれていない場合や、ユーザーが選択したフィルターに結果が見つか"
"らない場合です。これをご利用いただくことで、今後の日程調整のために連絡する方"
"法を宣伝できます。1文または2文以上は推奨いたしません。"
#: pretix/base/settings.py #: pretix/base/settings.py
msgid "Guidance text" msgid "Guidance text"
@@ -13897,20 +13902,26 @@ msgid ""
msgstr "ギフトカードの有効期限を発行年を含めた{}年に設定しました。" msgstr "ギフトカードの有効期限を発行年を含めた{}年に設定しました。"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment date"
msgid "Payment term" msgid "Payment term"
msgstr "支払条件" msgstr "支払い日"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "same as above" msgid "same as above"
msgstr "上記と同じ" msgstr ""
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in days"
msgid "different payment term in days" msgid "different payment term in days"
msgstr "日数で表す異なる支払条件" msgstr "支払い条件(日数)"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
#, fuzzy
#| msgid "Payment term in minutes"
msgid "different payment term in minutes" msgid "different payment term in minutes"
msgstr "分で表す異なる支払条件" msgstr "支払い期限(分単位)"
#: pretix/control/forms/event.py #: pretix/control/forms/event.py
msgid "Prices including tax" msgid "Prices including tax"
@@ -22522,24 +22533,28 @@ msgstr ""
"食事を提供する場合、ユーザーに食事制限について尋ねることができる一例です。" "食事を提供する場合、ユーザーに食事制限について尋ねることができる一例です。"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new per-ticket question" msgid "Create a new per-ticket question"
msgstr "チケットごとに新しい質問を作成" msgstr "新しい質問を作成"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Create a new question"
msgid "Create a new order-level question" msgid "Create a new order-level question"
msgstr "注文ごとに新しい質問を作成" msgstr "新しい質問を作成"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Delete question"
msgid "Per-ticket questions" msgid "Per-ticket questions"
msgstr "チケットごとの質問" msgstr "質問を削除"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"These questions are asked for every ticket, so possibly multiple times in " "These questions are asked for every ticket, so possibly multiple times in "
"the same order." "the same order."
msgstr "" msgstr ""
"これらの質問はすべてのチケットに対して尋ねられるため、同じ順序で複数回になる"
"可能性があります。"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "Create a new question" msgid "Create a new question"
@@ -22558,24 +22573,28 @@ msgid "All personalized products"
msgstr "すべてのパーソナライズされた製品" msgstr "すべてのパーソナライズされた製品"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "Include questions"
msgid "Per-order questions" msgid "Per-order questions"
msgstr "注文ごとの質問" msgstr "質問を含む"
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"This functionality is in active development and expected to change " "This functionality is in active development and expected to change "
"significantly over the coming months." "significantly over the coming months."
msgstr "この機能は現在開発が活発で、今後数か月で大幅に変化すると予想されています。" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
msgid "" msgid ""
"Per-order questions are currently not supported and will not be displayed in " "Per-order questions are currently not supported and will not be displayed in "
"pretixPOS." "pretixPOS."
msgstr "注文ごとの質問は現在サポートされておらず、pretixPOSでは表示されません。" msgstr ""
#: pretix/control/templates/pretixcontrol/items/questions.html #: pretix/control/templates/pretixcontrol/items/questions.html
#, fuzzy
#| msgid "The question has been reordered."
msgid "These questions are asked once per order." msgid "These questions are asked once per order."
msgstr "これらの質問は、注文ごとに1回ずつ尋ねられます。" msgstr "質問の並び順が変更されました。"
#: pretix/control/templates/pretixcontrol/items/quota.html #: pretix/control/templates/pretixcontrol/items/quota.html
#: pretix/control/templates/pretixcontrol/items/quota_edit.html #: pretix/control/templates/pretixcontrol/items/quota_edit.html
@@ -23167,8 +23186,10 @@ msgstr "(任意)"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html #: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html #: pretix/presale/templates/pretixpresale/event/checkout_questions.html
#: pretix/presale/templates/pretixpresale/event/order_modify.html #: pretix/presale/templates/pretixpresale/event/order_modify.html
#, fuzzy
#| msgid "Additional information"
msgid "Additional order information" msgid "Additional order information"
msgstr "追加の注文情報" msgstr "追加情報"
#: pretix/control/templates/pretixcontrol/order/delete.html #: pretix/control/templates/pretixcontrol/order/delete.html
msgid "Delete order" msgid "Delete order"
@@ -24835,13 +24856,14 @@ msgid "Hardware model"
msgstr "ハードウェアの機種" msgstr "ハードウェアの機種"
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
#, python-format #, fuzzy, python-format
#| msgid "Begin: %(time)s"
msgid "Last seen: %(time)s" msgid "Last seen: %(time)s"
msgstr "最後の閲覧: %(time)s" msgstr "開始: %(time)s"
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "No recent contact" msgid "No recent contact"
msgstr "最近の連絡なし" msgstr ""
#: pretix/control/templates/pretixcontrol/organizers/devices.html #: pretix/control/templates/pretixcontrol/organizers/devices.html
msgid "Not yet initialized" msgid "Not yet initialized"
@@ -28198,14 +28220,19 @@ msgstr ""
"に高いことを意味します。ドメインのDNS設定を更新すべきです。" "に高いことを意味します。ドメインのDNS設定を更新すべきです。"
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
#, fuzzy
#| msgid ""
#| "We did not find DMARC record for your domain. This means that there is a "
#| "very high chance most of the emails will be rejected or marked as spam. "
#| "You should update the DNS settings of your domain."
msgid "" msgid ""
"We did not find a DMARC record for your domain. This means that there is a " "We did not find a DMARC record for your domain. This means that there is a "
"very high chance most of the emails will be rejected or marked as spam. You " "very high chance most of the emails will be rejected or marked as spam. You "
"should update the DNS settings of your domain." "should update the DNS settings of your domain."
msgstr "" msgstr ""
"あなたのドメインのDMARCレコードが見つかりませんでした。これは、ほとんどの" "お客様のドメインのDMARCレコードが見つかりませんでした。これは、ほとんどのメー"
"メールが拒否されたかスパムとしてマークされた可能性が非常に高いことを意味しま" "ルが拒否されたりスパムとしてマークされたりする可能性が非常に高いことを意味し"
"す。ドメインのDNS設定を更新すべきです。" "ます。ドメインのDNS設定を更新すべきです。"
#: pretix/control/views/mailsetup.py #: pretix/control/views/mailsetup.py
msgid "The verification code was incorrect, please try again." msgid "The verification code was incorrect, please try again."
@@ -30879,7 +30906,7 @@ msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "Allow further payments during compliance hold" msgid "Allow further payments during compliance hold"
msgstr "コンプライアンス保留の間、追加の支払いを許可する" msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "" msgid ""
@@ -30889,20 +30916,16 @@ msgid ""
"attempts during that window. This might result in them being charged twice " "attempts during that window. This might result in them being charged twice "
"if the original payment is approved." "if the original payment is approved."
msgstr "" msgstr ""
"PayPalの不正防止は、個々の支払いの処理をかなりの期間ブロックする可能性があり"
"ます。この期間中、支払いは「保留中」とマークされています。顧客がその期間中に"
"別の支払い試行を開始できるように許可することができます。元の支払いが承認され"
"た場合、二重に請求される可能性があります。"
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "Timeout further payment attempts" msgid "Timeout further payment attempts"
msgstr "追加の支払い試行がタイムアウト" msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "" msgid ""
"Time duration in minutes after which another payment attempt is possible, " "Time duration in minutes after which another payment attempt is possible, "
"while the last payment is still under investigation." "while the last payment is still under investigation."
msgstr "最後の支払いがまだ調査中である間、別の支払い試行が可能となる時間(分)。" msgstr ""
#: pretix/plugins/paypal2/payment.py #: pretix/plugins/paypal2/payment.py
msgid "-- Automatic --" msgid "-- Automatic --"
@@ -31167,10 +31190,6 @@ msgid ""
"twice in case PayPal allows your initial payment attempt. Please contact us " "twice in case PayPal allows your initial payment attempt. Please contact us "
"to resolve this case." "to resolve this case."
msgstr "" msgstr ""
"支払いはPayPalで処理されています。通常より時間がかかります。PayPalが支払いを"
"確認するまでお待ちいただくか、こちらまたは別の支払い方法で再度お支払いをお試"
"しください。PayPalが最初の支払い試行を許可した場合、二重に請求される可能性が"
"あります。この件を解決するために、弊社までご連絡ください。"
#: pretix/plugins/paypal2/views.py #: pretix/plugins/paypal2/views.py
msgid "" msgid ""
+6 -11
View File
@@ -8,7 +8,7 @@ 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: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-02 08:35+0000\n" "PO-Revision-Date: 2026-03-30 21:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n" "Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/" "Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n" "pretix/pretix/pt_BR/>\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 2026.8.1\n" "X-Generator: Weblate 5.16.2\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "English" msgid "English"
@@ -93,7 +93,7 @@ msgstr "Hebraico"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Hungarian" msgid "Hungarian"
msgstr "Húngaro" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Indonesian" msgid "Indonesian"
@@ -153,7 +153,7 @@ msgstr "Espanhol (América latina)"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Thai" msgid "Thai"
msgstr "Tailandês" msgstr ""
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "Turkish" msgid "Turkish"
@@ -3022,12 +3022,11 @@ msgid ""
"The field \"%(label)s\" may not contain special characters such as " "The field \"%(label)s\" may not contain special characters such as "
"\"%(chars)s\"." "\"%(chars)s\"."
msgstr "" msgstr ""
"O campo \"%(label)s\" não pode conter caracteres especiais como %(chars)s\"."
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
#, python-format #, python-format
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)." msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
msgstr "O campo \"%(label)s\" não pode conter uma URL (%(url)s)." msgstr ""
#: pretix/base/forms/questions.py #: pretix/base/forms/questions.py
msgctxt "phonenumber" msgctxt "phonenumber"
@@ -8174,7 +8173,7 @@ msgstr "Permitida"
#: pretix/base/permissions.py #: pretix/base/permissions.py
msgctxt "permission_level" msgctxt "permission_level"
msgid "Access existing events" msgid "Access existing events"
msgstr "Acessar eventos existentes" msgstr ""
#: pretix/base/permissions.py #: pretix/base/permissions.py
msgctxt "permission_level" msgctxt "permission_level"
@@ -13608,15 +13607,11 @@ msgstr "Por favor, continue em uma nova aba"
#: pretix/base/templates/pretixbase/framebreak.html #: pretix/base/templates/pretixbase/framebreak.html
msgid "For security reasons, the following step is only possible in a new tab." msgid "For security reasons, the following step is only possible in a new tab."
msgstr "" msgstr ""
"Por razões de segurança, o passo seguinte só é possível de ser feito em uma "
"nova aba."
#: pretix/base/templates/pretixbase/framebreak.html #: pretix/base/templates/pretixbase/framebreak.html
msgid "" msgid ""
"If the new tab did not open automatically, please click the following button:" "If the new tab did not open automatically, please click the following button:"
msgstr "" msgstr ""
"Se uma nova aba não abrir automaticamente, por favor, clique no botão a "
"seguir:"
#: pretix/base/templates/pretixbase/framebreak.html #: pretix/base/templates/pretixbase/framebreak.html
#: pretix/presale/templates/pretixpresale/event/cookies.html #: pretix/presale/templates/pretixpresale/event/cookies.html
+4 -5
View File
@@ -8,8 +8,8 @@ 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: 2026-08-24 15:35+0000\n" "POT-Creation-Date: 2026-08-24 15:35+0000\n"
"PO-Revision-Date: 2026-09-04 14:29+0000\n" "PO-Revision-Date: 2026-08-09 06:00+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n" "Last-Translator: Julien <julien@circusiloveyou.com>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/" "Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/"
"sv/>\n" "sv/>\n"
"Language: sv\n" "Language: sv\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 2026.9\n" "X-Generator: Weblate 2026.8.1\n"
#: pretix/_base_settings.py #: pretix/_base_settings.py
msgid "English" msgid "English"
@@ -19170,9 +19170,8 @@ msgstr ""
#: pretix/presale/templates/pretixpresale/event/order_pay.html #: pretix/presale/templates/pretixpresale/event/order_pay.html
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html #: pretix/presale/templates/pretixpresale/event/order_pay_change.html
#: pretix/presale/templates/pretixpresale/event/position_change.html #: pretix/presale/templates/pretixpresale/event/position_change.html
#, fuzzy
msgid "Continue" msgid "Continue"
msgstr "Fortsätt" msgstr "Fortsätta"
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html #: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
msgid "Authorize an application" msgid "Authorize an application"
+4 -5
View File
@@ -8,8 +8,8 @@ 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: 2026-07-06 15:52+0000\n" "POT-Creation-Date: 2026-07-06 15:52+0000\n"
"PO-Revision-Date: 2026-09-04 14:29+0000\n" "PO-Revision-Date: 2026-08-09 06:00+0000\n"
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n" "Last-Translator: Julien <julien@circusiloveyou.com>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-" "Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/sv/>\n" "js/sv/>\n"
"Language: sv\n" "Language: sv\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 2026.9\n" "X-Generator: Weblate 2026.8.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js #: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
msgid "Marked as paid" msgid "Marked as paid"
@@ -133,9 +133,8 @@ msgstr "Mercado Pago"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts #: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
#: pretix/static/pretixpresale/js/ui/cart.js #: pretix/static/pretixpresale/js/ui/cart.js
#, fuzzy
msgid "Continue" msgid "Continue"
msgstr "Fortsätt" msgstr "Fortsätta"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js #: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js #: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
File diff suppressed because it is too large Load Diff
+9 -1
View File
@@ -55,6 +55,7 @@ from django.db.models.functions import Cast, Coalesce
from django.utils.timezone import make_aware from django.utils.timezone import make_aware
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
from pypdf import PageObject, PdfReader, PdfWriter, Transformation from pypdf import PageObject, PdfReader, PdfWriter, Transformation
from pypdf.generic import RectangleObject
from reportlab.lib import pagesizes from reportlab.lib import pagesizes
from reportlab.lib.units import inch, mm from reportlab.lib.units import inch, mm
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
@@ -237,8 +238,15 @@ def _render_nup_page(nup_pdf: PdfWriter, input_pages: PageObject, opt: dict) ->
di = i % badges_per_page di = i % badges_per_page
tx = opt['margins'][3] + (di % opt['cols']) * opt['offsets'][0] tx = opt['margins'][3] + (di % opt['cols']) * opt['offsets'][0]
ty = opt['margins'][2] + (opt['rows'] - 1 - (di // opt['cols'])) * opt['offsets'][1] ty = opt['margins'][2] + (opt['rows'] - 1 - (di // opt['cols'])) * opt['offsets'][1]
page.add_transformation(Transformation().translate(tx, ty))
page.mediabox = RectangleObject((
Decimal('%.5f' % (page.mediabox.left.as_numeric() + tx)),
Decimal('%.5f' % (page.mediabox.bottom.as_numeric() + ty)),
Decimal('%.5f' % (page.mediabox.right.as_numeric() + tx)),
Decimal('%.5f' % (page.mediabox.top.as_numeric() + ty))
))
page.trimbox = page.cropbox = page.mediabox page.trimbox = page.cropbox = page.mediabox
nup_page.merge_transformed_page(page, Transformation().translate(tx, ty)) nup_page.merge_page(page)
return nup_page return nup_page
@@ -1,183 +1,183 @@
/* global gettext */ /*global $, gettext*/
var bankimport_transactionlist = { var bankimport_transactionlist = {
_btn_click: function (e) { _btn_click: function (e) {
console.log(e.delegateTarget) console.log(e.delegateTarget);
let trans_id = parseInt($(e.delegateTarget).attr('name').split('_')[1]) var trans_id = parseInt($(e.delegateTarget).attr("name").split("_")[1]);
let value = $(e.delegateTarget).val() var value = $(e.delegateTarget).val();
if (value === 'discard') { if (value === "discard") {
bankimport_transactionlist.discard(trans_id) bankimport_transactionlist.discard(trans_id);
} else if (value === 'accept') { } else if (value === "accept") {
bankimport_transactionlist.accept(trans_id) bankimport_transactionlist.accept(trans_id);
} else if (value === 'retry') { } else if (value === "retry") {
bankimport_transactionlist.retry(trans_id) bankimport_transactionlist.retry(trans_id);
} else if (value === 'assign') { } else if (value === "assign") {
bankimport_transactionlist.assign(trans_id) bankimport_transactionlist.assign(trans_id);
} }
return false return false;
}, },
_action: function (id, action, success) { _action: function (id, action, success) {
$('tr[data-id=' + id + '] button').prop('disabled', true) $("tr[data-id=" + id + "] button").prop("disabled", true);
let data = { var data = {
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val() "csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
} };
data['action_' + id] = action data["action_" + id] = action;
$.ajax({ $.ajax({
method: 'POST', "method": "POST",
url: $('.transaction-list').attr('data-url'), "url": $(".transaction-list").attr("data-url"),
data: data, "data": data,
dataType: 'json', "dataType": "json",
success: function (data) { "success": function (data) {
if (data.status == 'ok') { if (data.status == "ok") {
$('tr[data-id=' + id + ']').removeClass('has-error') $("tr[data-id=" + id + "]").removeClass("has-error");
if (data.comment) { if (data.comment) {
bankimport_transactionlist.comment_reset_to_text(id, data.comment, data.plain) bankimport_transactionlist.comment_reset_to_text(id, data.comment, data.plain);
} }
success() success();
} else { } else {
$('tr[data-id=' + id + '] button').prop('disabled', false) $("tr[data-id=" + id + "] button").prop("disabled", false);
$('tr[data-id=' + id + '] .help-block').remove() $("tr[data-id=" + id + "] .help-block").remove();
$('tr[data-id=' + id + ']').addClass('has-error') $("tr[data-id=" + id + "]").addClass("has-error");
$('<p>').addClass('help-block').text(data.message).appendTo($('tr[data-id=' + id + '] td.actions')) $("<p>").addClass("help-block").text(data.message).appendTo($("tr[data-id=" + id + "] td.actions"));
} }
} }
}) });
}, },
discard: function (id) { discard: function (id) {
bankimport_transactionlist._action(id, 'discard', function () { bankimport_transactionlist._action(id, "discard", function () {
$('tr[data-id=' + id + '] td').remove() $("tr[data-id=" + id + "] td").remove();
}) });
}, },
retry: function (id) { retry: function (id) {
bankimport_transactionlist._action(id, 'retry', function () { bankimport_transactionlist._action(id, "retry", function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid')) $("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
}) });
}, },
accept: function (id) { accept: function (id) {
bankimport_transactionlist._action(id, 'accept', function () { bankimport_transactionlist._action(id, "accept", function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid')) $("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
}) });
}, },
assign: function (id) { assign: function (id) {
bankimport_transactionlist._action(id, 'assign:' + $('tr[data-id=' + id + '] input.form-control:not(.tt-hint)').val(), function () { bankimport_transactionlist._action(id, "assign:" + $("tr[data-id=" + id + "] input.form-control:not(.tt-hint)").val(), function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid')) $("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
}) });
}, },
comment_reset_to_text: function (id, text, plain) { comment_reset_to_text: function (id, text, plain) {
let $box = $('tr[data-id=' + id + '] .comment-box') var $box = $("tr[data-id=" + id + "] .comment-box");
$box[0].dataset['plain'] = plain $box[0].dataset["plain"] = plain;
$box.html('') $box.html("")
.append($('<strong>').text(gettext('Comment:'))) .append($("<strong>").text(gettext("Comment:")))
.append(' ') .append(" ")
.append($('<span>').addClass('comment').append(' ').append(text)) .append($("<span>").addClass("comment").append(" ").append(text))
.append(' ') .append(" ")
.append($('<a>').addClass('comment-modify btn btn-default btn-xs') .append($("<a>").addClass("comment-modify btn btn-default btn-xs")
.append('<span class=\'fa fa-edit\'></span>')) .append("<span class='fa fa-edit'></span>"));
}, },
comment_start_edit: function (e) { comment_start_edit: function (e) {
let $box = $(e.target).closest('div') var $box = $(e.target).closest("div");
let id = $box.closest('tr').attr('data-id') var id = $box.closest("tr").attr("data-id");
let $inp = $('<textarea>').addClass('form-control') var $inp = $("<textarea>").addClass("form-control");
let orig_rendered = $box.find('.comment') var orig_rendered = $box.find(".comment");
let orig_text = $box[0].dataset.plain var orig_text = $box[0].dataset.plain;
$inp.val(orig_text) $inp.val(orig_text);
let $btngrp = $('<div>') var $btngrp = $("<div>");
$btngrp.addClass('btn-group') $btngrp.addClass("btn-group");
let $btn1 = $('<button>') var $btn1 = $("<button>");
$btn1.attr('type', 'button').addClass('btn btn-default') $btn1.attr("type", "button").addClass("btn btn-default");
$btn1.append('<span class=\'fa fa-check\'></span>') $btn1.append("<span class='fa fa-check'></span>");
$btngrp.append($btn1) $btngrp.append($btn1);
let $btn2 = $('<button>') var $btn2 = $("<button>");
$btn2.attr('type', 'button').addClass('btn btn-default') $btn2.attr("type", "button").addClass("btn btn-default");
$btn2.append('<span class=\'fa fa-close\'></span>') $btn2.append("<span class='fa fa-close'></span>");
$btngrp.append($btn2) $btngrp.append($btn2);
$box.html('').append($inp).append($btngrp) $box.html("").append($inp).append($btngrp);
$btn1.click(function () { $btn1.click(function () {
let text = $box.find('textarea').val() var text = $box.find("textarea").val();
$box.find('input, textarea, button').prop('disabled', true) $box.find("input, textarea, button").prop("disabled", true);
bankimport_transactionlist._action(id, 'comment:' + text, function () { bankimport_transactionlist._action(id, "comment:" + text, function () {
$('tr[data-id=' + id + '] button').prop('disabled', false) $("tr[data-id=" + id + "] button").prop("disabled", false);
}) });
}) });
$btn2.click(function () { $btn2.click(function () {
bankimport_transactionlist.comment_reset_to_text(id, orig_rendered, orig_text) bankimport_transactionlist.comment_reset_to_text(id, orig_rendered, orig_text);
}) });
e.preventDefault() e.preventDefault();
}, },
typeahead_source: function () { typeahead_source: function () {
return new Bloodhound({ return new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace, queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: { remote: {
url: $('.transaction-list').attr('data-url'), url: $(".transaction-list").attr("data-url"),
prepare: function (query, settings) { prepare: function (query, settings) {
settings.url = settings.url + '?query=' + encodeURIComponent(query) settings.url = settings.url + '?query=' + encodeURIComponent(query);
return settings return settings;
}, },
transform: function (object) { transform: function (object) {
let results = object.results var results = object.results;
let suggs = [] var suggs = [];
let reslen = results.length var reslen = results.length;
for (let i = 0; i < reslen; i++) { for (var i = 0; i < reslen; i++) {
suggs.push(results[i]) suggs.push(results[i]);
} }
return suggs return suggs;
} }
} }
}) });
}, },
init: function () { init: function () {
if ($('.transaction-list').length) { if ($(".transaction-list").length) {
$('.transaction-list button').click(bankimport_transactionlist._btn_click) $(".transaction-list button").click(bankimport_transactionlist._btn_click);
$('.transaction-list').on('click', '.comment-modify', bankimport_transactionlist.comment_start_edit) $(".transaction-list").on("click", ".comment-modify", bankimport_transactionlist.comment_start_edit);
$('.transaction-list .form-control').typeahead(null, { $(".transaction-list .form-control").typeahead(null, {
minLength: 2, minLength: 2,
name: 'order-dataset', name: 'order-dataset',
source: bankimport_transactionlist.typeahead_source(), source: bankimport_transactionlist.typeahead_source(),
display: function (obj) { display: function (obj) {
return obj.code return obj.code;
}, },
templates: { templates: {
suggestion: function (obj) { suggestion: function (obj) {
return '<div>' + obj.code + ' (' + obj.total + ', ' + obj.status + ')</div>' return '<div>' + obj.code + ' (' + obj.total + ', ' + obj.status + ')</div>';
} }
} }
}).keypress(function (e) { }).keypress(function (e) {
if (e.keyCode === 13) { if (e.keyCode === 13) {
$(this).parent().parent().find('button[value=assign]').click() $(this).parent().parent().find("button[value=assign]").click();
} }
}) });
} }
if ($('[data-job-waiting]').length) { if ($("[data-job-waiting]").length) {
window.setTimeout(bankimport_transactionlist.check_state, 750) window.setTimeout(bankimport_transactionlist.check_state, 750);
} }
}, },
check_state: function () { check_state: function () {
$.getJSON($('[data-job-waiting-url]').attr('data-job-waiting-url'), function (data) { $.getJSON($("[data-job-waiting-url]").attr("data-job-waiting-url"), function (data) {
if (data.state == 'running' || data.state == 'pending') { if (data.state == 'running' || data.state == 'pending') {
window.setTimeout(bankimport_transactionlist.check_state, 750) window.setTimeout(bankimport_transactionlist.check_state, 750);
} else { } else {
location.reload() location.reload();
} }
}) });
} }
} };
$(function () { $(function () {
bankimport_transactionlist.init() bankimport_transactionlist.init();
}) });
+12
View File
@@ -19,3 +19,15 @@
# 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/>.
# #
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
+53
View File
@@ -0,0 +1,53 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix 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 hashlib
import json
from django.core.cache import cache
from paypalrestsdk.api import Api as VendorApi
class Api(VendorApi):
def get_token_hash(self, authorization_code=None, refresh_token=None, headers=None):
if not authorization_code and not refresh_token:
checksum = hashlib.sha256(self.basic_auth().encode()).hexdigest()
cache_key_hash = f'pretix_paypal_token_hash_{checksum}'
cache_key_request_at = f'pretix_paypal_token_request_at_{checksum}'
token_hash = cache.get(cache_key_hash)
if token_hash:
token_request_at = cache.get(cache_key_request_at)
if token_request_at:
self.token_hash = json.loads(token_hash)
self.token_request_at = token_request_at
self.validate_token_hash()
if self.token_hash is not None:
return self.token_hash
t = super().get_token_hash(authorization_code, refresh_token, headers)
if self.token_hash:
cache.set(cache_key_hash, json.dumps(self.token_hash), 3600 * 4)
cache.set(cache_key_request_at, self.token_request_at, 3600 * 4)
return t
return super().get_token_hash(authorization_code, refresh_token, headers)
+69
View File
@@ -0,0 +1,69 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Tobias Kunze
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from django.apps import AppConfig
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from pretix import __version__ as version
class PaypalApp(AppConfig):
name = 'pretix.plugins.paypal'
verbose_name = _("PayPal")
class PretixPluginMeta:
name = _("PayPal")
author = _("the pretix team")
version = version
category = 'PAYMENT'
featured = True
picture = 'pretixplugins/paypal/paypal_logo.svg'
description = _("Accept payments with your PayPal account. PayPal is one of the most popular payment methods "
"world-wide.")
def ready(self):
from . import signals # NOQA
def is_available(self, event):
return 'pretix.plugins.paypal' in event.plugins.split(',')
@cached_property
def compatibility_errors(self):
errs = []
try:
import paypalrestsdk # NOQA
except ImportError:
errs.append("Python package 'paypalrestsdk' is not installed.")
return errs
@@ -1,21 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-08 08:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("paypal", "0004_bigint"),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.DeleteModel(
name="ReferencedPayPalObject",
),
],
database_operations=[]
)
]
@@ -26,6 +26,3 @@ class ReferencedPayPalObject(models.Model):
reference = models.CharField(max_length=190, db_index=True, unique=True) reference = models.CharField(max_length=190, db_index=True, unique=True)
order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE) order = models.ForeignKey('pretixbase.Order', on_delete=models.CASCADE)
payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE) payment = models.ForeignKey('pretixbase.OrderPayment', null=True, blank=True, on_delete=models.CASCADE)
class Meta:
db_table = 'paypal_referencedpaypalobject'
+715
View File
@@ -0,0 +1,715 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Jakob Schnell, Tobias Kunze
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import json
import logging
from collections import OrderedDict
from decimal import Decimal
import paypalrestsdk
import paypalrestsdk.exceptions
from django import forms
from django.contrib import messages
from django.http import HttpRequest
from django.template.loader import get_template
from django.urls import reverse
from django.utils.html import format_html
from django.utils.timezone import now
from django.utils.translation import gettext as __, gettext_lazy as _
from i18nfield.strings import LazyI18nString
from paypalrestsdk.exceptions import BadRequest, UnauthorizedAccess
from paypalrestsdk.openid_connect import Tokeninfo
from requests import RequestException
from pretix.base.decimal import round_decimal
from pretix.base.forms import SecretKeySettingsField
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.settings import SettingsSandbox
from pretix.base.views.redirect import safelink
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.plugins.paypal.api import Api
from pretix.plugins.paypal.models import ReferencedPayPalObject
logger = logging.getLogger('pretix.plugins.paypal')
SUPPORTED_CURRENCIES = ['AUD', 'BRL', 'CAD', 'CZK', 'DKK', 'EUR', 'HKD', 'HUF', 'INR', 'ILS', 'JPY', 'MYR', 'MXN',
'TWD', 'NZD', 'NOK', 'PHP', 'PLN', 'GBP', 'RUB', 'SGD', 'SEK', 'CHF', 'THB', 'USD']
LOCAL_ONLY_CURRENCIES = ['INR']
class Paypal(BasePaymentProvider):
identifier = 'paypal'
verbose_name = _('PayPal')
payment_form_fields = OrderedDict([
])
def __init__(self, event: Event):
super().__init__(event)
self.settings = SettingsSandbox('payment', 'paypal', event)
@property
def test_mode_message(self):
if self.settings.connect_client_id and not self.settings.secret:
# in OAuth mode, sandbox mode needs to be set global
is_sandbox = self.settings.connect_endpoint == 'sandbox'
else:
is_sandbox = self.settings.get('endpoint') == 'sandbox'
if is_sandbox:
return _('The PayPal sandbox is being used, you can test without actually sending money but you will need a '
'PayPal sandbox user to log in.')
return None
@property
def settings_form_fields(self):
if self.settings.connect_client_id and not self.settings.secret:
# PayPal connect
if self.settings.connect_user_id:
fields = [
('connect_user_id',
forms.CharField(
label=_('PayPal account'),
disabled=True
)),
]
else:
return {}
else:
fields = [
('client_id',
forms.CharField(
label=_('Client ID'),
min_length=80,
help_text=format_html(
'<a target="_blank" rel="noopener" href="{docs_url}">{text}</a>',
text=_('Click here for a tutorial on how to obtain the required keys'),
docs_url='https://docs.pretix.eu/en/latest/user/payments/paypal.html',
)
)),
('secret',
SecretKeySettingsField(
label=_('Secret'),
max_length=80,
min_length=80,
)),
('endpoint',
forms.ChoiceField(
label=_('Endpoint'),
initial='live',
choices=(
('live', 'Live'),
('sandbox', 'Sandbox'),
),
)),
]
extra_fields = [
('prefix',
forms.CharField(
label=_('Reference prefix'),
help_text=_('Any value entered here will be added in front of the regular booking reference '
'containing the order number.'),
required=False,
)),
('postfix',
forms.CharField(
label=_('Reference postfix'),
help_text=_('Any value entered here will be added behind the regular booking reference '
'containing the order number.'),
required=False,
)),
]
d = OrderedDict(
fields + extra_fields + list(super().settings_form_fields.items())
)
d.move_to_end('prefix')
d.move_to_end('postfix')
d.move_to_end('_enabled', False)
return d
def get_connect_url(self, request):
request.session['payment_paypal_oauth_event'] = request.event.pk
self.init_api()
return Tokeninfo.authorize_url({'scope': 'openid profile email'})
def settings_content_render(self, request):
settings_content = ""
if self.settings.connect_client_id and not self.settings.secret:
# Use PayPal connect
if not self.settings.connect_user_id:
# Migrate User to PayPal v2
self.event.disable_plugin("pretix.plugins.paypal")
self.event.enable_plugin("pretix.plugins.paypal2")
self.event.save()
else:
settings_content = (
"<button formaction='{}' class='btn btn-danger'>{}</button>"
).format(
reverse('plugins:paypal:oauth.disconnect', kwargs={
'organizer': self.event.organizer.slug,
'event': self.event.slug,
}),
_('Disconnect from PayPal')
)
else:
# Migrate User to PayPal v2
self.event.disable_plugin("pretix.plugins.paypal")
self.event.enable_plugin("pretix.plugins.paypal2")
self.event.save()
return settings_content
def is_allowed(self, request: HttpRequest, total: Decimal = None) -> bool:
return super().is_allowed(request, total) and self.event.currency in SUPPORTED_CURRENCIES
def init_api(self):
if self.settings.connect_client_id and not self.settings.secret:
paypalrestsdk.api.__api__ = Api(
mode="sandbox" if "sandbox" in self.settings.connect_endpoint else 'live',
client_id=self.settings.connect_client_id,
client_secret=self.settings.connect_secret_key,
openid_client_id=self.settings.connect_client_id,
openid_client_secret=self.settings.connect_secret_key
)
else:
paypalrestsdk.api.__api__ = Api(
mode="sandbox" if "sandbox" in self.settings.get('endpoint') else 'live',
client_id=self.settings.get('client_id'),
client_secret=self.settings.get('secret')
)
def payment_is_valid_session(self, request):
return (request.session.get('payment_paypal_id', '') != ''
and request.session.get('payment_paypal_payer', '') != '')
def payment_form_render(self, request) -> str:
template = get_template('pretixplugins/paypal/checkout_payment_form.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
def checkout_prepare(self, request, cart):
self.init_api()
kwargs = {}
if request.resolver_match and 'cart_namespace' in request.resolver_match.kwargs:
kwargs['cart_namespace'] = request.resolver_match.kwargs['cart_namespace']
try:
if self.settings.connect_client_id and not self.settings.secret:
if not request.event.settings.payment_paypal_connect_user_id:
raise PaymentException('Payment method misconfigured')
try:
tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
except BadRequest as ex:
ex = json.loads(ex.content)
messages.error(request, '{}: {} ({})'.format(
_('We had trouble communicating with PayPal'),
ex['error_description'],
ex['correlation_id'])
)
return
# Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
# get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
try:
userinfo = tokeninfo.userinfo()
request.event.settings.payment_paypal_connect_user_id = userinfo.email
except UnauthorizedAccess:
pass
payee = {
"email": request.event.settings.payment_paypal_connect_user_id,
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
# we should use it instead of the merchant's eMail-address
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
}
else:
payee = {}
payment = paypalrestsdk.Payment({
'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
'intent': 'sale',
'payer': {
"payment_method": "paypal",
},
"redirect_urls": {
"return_url": eventreverse_absolute(request.event, 'plugins:paypal:return', kwargs=kwargs),
"cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort', kwargs=kwargs),
},
"transactions": [
{
"item_list": {
"items": [
{
"name": '{prefix}{orderstring}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
orderstring=__('Order for %s') % str(request.event),
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
),
"quantity": 1,
"price": self.format_price(cart['total']),
"currency": request.event.currency
}
]
},
"amount": {
"currency": request.event.currency,
"total": self.format_price(cart['total'])
},
"description": __('Event tickets for {event}').format(event=request.event.name),
"payee": payee,
"custom": '{prefix}{slug}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
slug=request.event.slug.upper(),
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
)
}
]
})
request.session['payment_paypal_payment'] = None
return self._create_payment(request, payment)
except paypalrestsdk.exceptions.ConnectionError as e:
messages.error(request, _('We had trouble communicating with PayPal'))
logger.exception('Error on creating payment: ' + str(e))
def format_price(self, value):
return str(round_decimal(value, self.event.currency, {
# PayPal behaves differently than Stripe in deciding what currencies have decimal places
# Source https://developer.paypal.com/docs/classic/api/currency_codes/
'HUF': 0,
'JPY': 0,
'MYR': 0,
'TWD': 0,
# However, CLPs are not listed there while PayPal requires us not to send decimal places there. WTF.
'CLP': 0,
# Let's just guess that the ones listed here are 0-based as well
# https://developers.braintreepayments.com/reference/general/currencies
'BIF': 0,
'DJF': 0,
'GNF': 0,
'KMF': 0,
'KRW': 0,
'LAK': 0,
'PYG': 0,
'RWF': 0,
'UGX': 0,
'VND': 0,
'VUV': 0,
'XAF': 0,
'XOF': 0,
'XPF': 0,
}))
@property
def abort_pending_allowed(self):
return False
def _create_payment(self, request, payment):
if payment.create():
if payment.state not in ('created', 'approved', 'pending'):
messages.error(request, _('We had trouble communicating with PayPal'))
logger.error('Invalid payment state: ' + str(payment))
return
request.session['payment_paypal_id'] = payment.id
for link in payment.links:
if link.method == "REDIRECT" and link.rel == "approval_url":
if request.session.get('iframe_session', False):
return safelink(link.href, framebreak=True)
else:
return str(link.href)
else:
messages.error(request, _('We had trouble communicating with PayPal'))
logger.error('Error on creating payment: ' + str(payment.error))
def checkout_confirm_render(self, request) -> str:
"""
Returns the HTML that should be displayed when the user selected this provider
on the 'confirm order' page.
"""
template = get_template('pretixplugins/paypal/checkout_payment_confirm.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings}
return template.render(ctx)
def execute_payment(self, request: HttpRequest, payment: OrderPayment):
if (request.session.get('payment_paypal_id', '') == '' or request.session.get('payment_paypal_payer', '') == ''):
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
'proceed.'))
self.init_api()
pp_payment = paypalrestsdk.Payment.find(request.session.get('payment_paypal_id'))
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=pp_payment.id)
if str(pp_payment.transactions[0].amount.total) != str(payment.amount) or pp_payment.transactions[0].amount.currency \
!= self.event.currency:
logger.error('Value mismatch: Payment %s vs paypal trans %s' % (payment.id, str(pp_payment)))
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
'proceed.'))
return self._execute_payment(pp_payment, request, payment)
def _execute_payment(self, payment, request, payment_obj):
if payment.state == 'created':
payment.replace([
{
"op": "replace",
"path": "/transactions/0/item_list",
"value": {
"items": [
{
"name": '{prefix}{orderstring}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
orderstring=__('Order {slug}-{code}').format(
slug=self.event.slug.upper(),
code=payment_obj.order.code
),
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
),
"quantity": 1,
"price": self.format_price(payment_obj.amount),
"currency": payment_obj.order.event.currency
}
]
}
},
{
"op": "replace",
"path": "/transactions/0/description",
"value": '{prefix}{orderstring}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
orderstring=__('Order {order} for {event}').format(
event=request.event.name,
order=payment_obj.order.code
),
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
),
}
])
try:
payment.execute({"payer_id": request.session.get('payment_paypal_payer')})
except paypalrestsdk.exceptions.ConnectionError as e:
messages.error(request, _('We had trouble communicating with PayPal'))
logger.exception('Error on creating payment: ' + str(e))
except RequestException as e:
messages.error(request, _('We had trouble communicating with PayPal'))
logger.exception('Error on creating payment: ' + str(e))
for trans in payment.transactions:
for rr in trans.related_resources:
if hasattr(rr, 'sale') and rr.sale:
if rr.sale.state == 'pending':
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
'soon as the payment completed.'))
payment_obj.info = json.dumps(payment.to_dict())
payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
payment_obj.save()
return
payment_obj.refresh_from_db()
if payment.state == 'pending':
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as soon as the '
'payment completed.'))
payment_obj.info = json.dumps(payment.to_dict())
payment_obj.state = OrderPayment.PAYMENT_STATE_PENDING
payment_obj.save()
return
if payment.state != 'approved':
payment_obj.fail(info=payment.to_dict())
logger.error('Invalid state: %s' % str(payment))
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
'proceed.'))
if payment_obj.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
logger.warning('PayPal success event even though order is already marked as paid')
return
try:
payment_obj.info = json.dumps(payment.to_dict())
payment_obj.save(update_fields=['info'])
payment_obj.confirm()
except Quota.QuotaExceededException as e:
raise PaymentException(str(e))
return None
def payment_pending_render(self, request, payment) -> str:
retry = True
try:
if (
payment.info
and payment.info_data['transactions'][0]['related_resources'][0]['sale']['state'] == 'pending'
):
retry = False
except (KeyError, IndexError):
pass
template = get_template('pretixplugins/paypal/pending.html')
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
'retry': retry, 'order': payment.order}
return template.render(ctx)
def matching_id(self, payment: OrderPayment):
sale_id = None
for trans in payment.info_data.get('transactions', []):
for res in trans.get('related_resources', []):
if 'sale' in res and 'id' in res['sale']:
sale_id = res['sale']['id']
return sale_id or payment.info_data.get('id', None)
def api_payment_details(self, payment: OrderPayment):
sale_id = None
for trans in payment.info_data.get('transactions', []):
for res in trans.get('related_resources', []):
if 'sale' in res and 'id' in res['sale']:
sale_id = res['sale']['id']
return {
"payer_email": payment.info_data.get('payer', {}).get('payer_info', {}).get('email'),
"payer_id": payment.info_data.get('payer', {}).get('payer_info', {}).get('payer_id'),
"cart_id": payment.info_data.get('cart', None),
"payment_id": payment.info_data.get('id', None),
"sale_id": sale_id,
}
def payment_control_render(self, request: HttpRequest, payment: OrderPayment):
template = get_template('pretixplugins/paypal/control.html')
sale_id = None
for trans in payment.info_data.get('transactions', []):
for res in trans.get('related_resources', []):
if 'sale' in res and 'id' in res['sale']:
sale_id = res['sale']['id']
ctx = {'request': request, 'event': self.event, 'settings': self.settings,
'payment_info': payment.info_data, 'order': payment.order, 'sale_id': sale_id}
return template.render(ctx)
def payment_control_render_short(self, payment: OrderPayment) -> str:
return payment.info_data.get('payer', {}).get('payer_info', {}).get('email', '')
def payment_partial_refund_supported(self, payment: OrderPayment):
# Paypal refunds are possible for 180 days after purchase:
# https://www.paypal.com/lc/smarthelp/article/how-do-i-issue-a-refund-faq780#:~:text=Refund%20after%20180%20days%20of,PayPal%20balance%20of%20the%20recipient.
return (now() - payment.payment_date).days <= 180
def payment_refund_supported(self, payment: OrderPayment):
self.payment_partial_refund_supported(payment)
def execute_refund(self, refund: OrderRefund):
self.init_api()
try:
sale = None
for res in refund.payment.info_data['transactions'][0]['related_resources']:
for k, v in res.items():
if k == 'sale':
sale = paypalrestsdk.Sale.find(v['id'])
break
if not sale:
pp_payment = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
for res in pp_payment.transactions[0].related_resources:
for k, v in res.to_dict().items():
if k == 'sale':
sale = paypalrestsdk.Sale.find(v['id'])
break
pp_refund = sale.refund({
"amount": {
"total": self.format_price(refund.amount),
"currency": refund.order.event.currency
}
})
except paypalrestsdk.exceptions.ConnectionError as e:
refund.order.log_action('pretix.event.order.refund.failed', {
'local_id': refund.local_id,
'provider': refund.provider,
'error': str(e)
})
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(str(e)))
if not pp_refund.success():
refund.order.log_action('pretix.event.order.refund.failed', {
'local_id': refund.local_id,
'provider': refund.provider,
'error': str(pp_refund.error)
})
raise PaymentException(_('Refunding the amount via PayPal failed: {}').format(pp_refund.error))
else:
sale = paypalrestsdk.Payment.find(refund.payment.info_data['id'])
refund.payment.info = json.dumps(sale.to_dict())
refund.info = json.dumps(pp_refund.to_dict())
refund.done()
def payment_prepare(self, request, payment_obj):
self.init_api()
try:
if request.event.settings.payment_paypal_connect_user_id:
try:
tokeninfo = Tokeninfo.create_with_refresh_token(request.event.settings.payment_paypal_connect_refresh_token)
except BadRequest as ex:
ex = json.loads(ex.content)
messages.error(request, '{}: {} ({})'.format(
_('We had trouble communicating with PayPal'),
ex['error_description'],
ex['correlation_id'])
)
return
# Even if the token has been refreshed, calling userinfo() can fail. In this case we just don't
# get the userinfo again and use the payment_paypal_connect_user_id that we already have on file
try:
userinfo = tokeninfo.userinfo()
request.event.settings.payment_paypal_connect_user_id = userinfo.email
except UnauthorizedAccess:
pass
payee = {
"email": request.event.settings.payment_paypal_connect_user_id,
# If PayPal ever offers a good way to get the MerchantID via the Identifity API,
# we should use it instead of the merchant's eMail-address
# "merchant_id": request.event.settings.payment_paypal_connect_user_id,
}
else:
payee = {}
payment = paypalrestsdk.Payment({
'header': {'PayPal-Partner-Attribution-Id': 'ramiioSoftwareentwicklung_SP'},
'intent': 'sale',
'payer': {
"payment_method": "paypal",
},
"redirect_urls": {
"return_url": eventreverse_absolute(request.event, 'plugins:paypal:return'),
"cancel_url": eventreverse_absolute(request.event, 'plugins:paypal:abort'),
},
"transactions": [
{
"item_list": {
"items": [
{
"name": '{prefix}{orderstring}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
orderstring=__('Order {slug}-{code}').format(
slug=self.event.slug.upper(),
code=payment_obj.order.code
),
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
),
"quantity": 1,
"price": self.format_price(payment_obj.amount),
"currency": payment_obj.order.event.currency
}
]
},
"amount": {
"currency": request.event.currency,
"total": self.format_price(payment_obj.amount)
},
"description": '{prefix}{orderstring}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
orderstring=__('Order {order} for {event}').format(
event=request.event.name,
order=payment_obj.order.code
),
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
),
"payee": payee,
"custom": '{prefix}{slug}-{code}{postfix}'.format(
prefix='{} '.format(self.settings.prefix) if self.settings.prefix else '',
slug=self.event.slug.upper(),
code=payment_obj.order.code,
postfix=' {}'.format(self.settings.postfix) if self.settings.postfix else ''
),
}
]
})
request.session['payment_paypal_payment'] = payment_obj.pk
return self._create_payment(request, payment)
except paypalrestsdk.exceptions.ConnectionError as e:
messages.error(request, _('We had trouble communicating with PayPal'))
logger.exception('Error on creating payment: ' + str(e))
def shred_payment_info(self, obj):
if obj.info:
d = json.loads(obj.info)
new = {
'id': d.get('id'),
'payer': {
'payer_info': {
'email': '█'
}
},
'update_time': d.get('update_time'),
'transactions': [
{
'amount': t.get('amount')
} for t in d.get('transactions', [])
],
'_shredded': True
}
obj.info = json.dumps(new)
obj.save(update_fields=['info'])
for le in obj.order.all_logentries().filter(action_type="pretix.plugins.paypal.event").exclude(data=""):
d = le.parsed_data
if 'resource' in d:
d['resource'] = {
'id': d['resource'].get('id'),
'sale_id': d['resource'].get('sale_id'),
'parent_payment': d['resource'].get('parent_payment'),
}
le.data = json.dumps(d)
le.shredded = True
le.save(update_fields=['data', 'shredded'])
def render_invoice_text(self, order: Order, payment: OrderPayment) -> str:
if order.status == Order.STATUS_PAID:
if payment.info_data.get('id', None):
try:
return '{}\r\n{}: {}\r\n{}: {}'.format(
_('The payment for this invoice has already been received.'),
_('PayPal payment ID'),
payment.info_data['id'],
_('PayPal sale ID'),
payment.info_data['transactions'][0]['related_resources'][0]['sale']['id']
)
except (KeyError, IndexError):
return '{}\r\n{}: {}'.format(
_('The payment for this invoice has already been received.'),
_('PayPal payment ID'),
payment.info_data['id']
)
else:
return super().render_invoice_text(order, payment)
return self.settings.get('_invoice_text', as_type=LazyI18nString, default='')
+31
View File
@@ -0,0 +1,31 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.dispatch import receiver
from pretix.base.signals import register_payment_providers
@receiver(register_payment_providers, dispatch_uid="payment_paypal")
def register_payment_provider(sender, **kwargs):
from .payment import Paypal
return Paypal
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 16.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="124px" height="33px" viewBox="0 0 124 33" enable-background="new 0 0 124 33" xml:space="preserve">
<path fill="#253B80" d="M46.211,6.749h-6.839c-0.468,0-0.866,0.34-0.939,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.564,0.658 h3.265c0.468,0,0.866-0.34,0.939-0.803l0.746-4.73c0.072-0.463,0.471-0.803,0.938-0.803h2.165c4.505,0,7.105-2.18,7.784-6.5 c0.306-1.89,0.013-3.375-0.872-4.415C50.224,7.353,48.5,6.749,46.211,6.749z M47,13.154c-0.374,2.454-2.249,2.454-4.062,2.454 h-1.032l0.724-4.583c0.043-0.277,0.283-0.481,0.563-0.481h0.473c1.235,0,2.4,0,3.002,0.704C47.027,11.668,47.137,12.292,47,13.154z"/>
<path fill="#253B80" d="M66.654,13.075h-3.275c-0.279,0-0.52,0.204-0.563,0.481l-0.145,0.916l-0.229-0.332 c-0.709-1.029-2.29-1.373-3.868-1.373c-3.619,0-6.71,2.741-7.312,6.586c-0.313,1.918,0.132,3.752,1.22,5.031 c0.998,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.562,0.66h2.95 c0.469,0,0.865-0.34,0.939-0.803l1.77-11.209C67.271,13.388,67.004,13.075,66.654,13.075z M62.089,19.449 c-0.316,1.871-1.801,3.127-3.695,3.127c-0.951,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.668-1.391-0.514-2.301 c0.295-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C62.034,17.721,62.232,18.543,62.089,19.449z"/>
<path fill="#253B80" d="M84.096,13.075h-3.291c-0.314,0-0.609,0.156-0.787,0.417l-4.539,6.686l-1.924-6.425 c-0.121-0.402-0.492-0.678-0.912-0.678h-3.234c-0.393,0-0.666,0.384-0.541,0.754l3.625,10.638l-3.408,4.811 c-0.268,0.379,0.002,0.9,0.465,0.9h3.287c0.312,0,0.604-0.152,0.781-0.408L84.564,13.97C84.826,13.592,84.557,13.075,84.096,13.075z "/>
<path fill="#179BD7" d="M94.992,6.749h-6.84c-0.467,0-0.865,0.34-0.938,0.802l-2.766,17.537c-0.055,0.346,0.213,0.658,0.562,0.658 h3.51c0.326,0,0.605-0.238,0.656-0.562l0.785-4.971c0.072-0.463,0.471-0.803,0.938-0.803h2.164c4.506,0,7.105-2.18,7.785-6.5 c0.307-1.89,0.012-3.375-0.873-4.415C99.004,7.353,97.281,6.749,94.992,6.749z M95.781,13.154c-0.373,2.454-2.248,2.454-4.062,2.454 h-1.031l0.725-4.583c0.043-0.277,0.281-0.481,0.562-0.481h0.473c1.234,0,2.4,0,3.002,0.704 C95.809,11.668,95.918,12.292,95.781,13.154z"/>
<path fill="#179BD7" d="M115.434,13.075h-3.273c-0.281,0-0.52,0.204-0.562,0.481l-0.145,0.916l-0.23-0.332 c-0.709-1.029-2.289-1.373-3.867-1.373c-3.619,0-6.709,2.741-7.311,6.586c-0.312,1.918,0.131,3.752,1.219,5.031 c1,1.176,2.426,1.666,4.125,1.666c2.916,0,4.533-1.875,4.533-1.875l-0.146,0.91c-0.055,0.348,0.213,0.66,0.564,0.66h2.949 c0.467,0,0.865-0.34,0.938-0.803l1.771-11.209C116.053,13.388,115.785,13.075,115.434,13.075z M110.869,19.449 c-0.314,1.871-1.801,3.127-3.695,3.127c-0.949,0-1.711-0.305-2.199-0.883c-0.484-0.574-0.666-1.391-0.514-2.301 c0.297-1.855,1.805-3.152,3.67-3.152c0.93,0,1.686,0.309,2.184,0.892C110.816,17.721,111.014,18.543,110.869,19.449z"/>
<path fill="#179BD7" d="M119.295,7.23l-2.807,17.858c-0.055,0.346,0.213,0.658,0.562,0.658h2.822c0.469,0,0.867-0.34,0.939-0.803 l2.768-17.536c0.055-0.346-0.213-0.659-0.562-0.659h-3.16C119.578,6.749,119.338,6.953,119.295,7.23z"/>
<path fill="#253B80" d="M7.266,29.154l0.523-3.322l-1.165-0.027H1.061L4.927,1.292C4.939,1.218,4.978,1.149,5.035,1.1 c0.057-0.049,0.13-0.076,0.206-0.076h9.38c3.114,0,5.263,0.648,6.385,1.927c0.526,0.6,0.861,1.227,1.023,1.917 c0.17,0.724,0.173,1.589,0.007,2.644l-0.012,0.077v0.676l0.526,0.298c0.443,0.235,0.795,0.504,1.065,0.812 c0.45,0.513,0.741,1.165,0.864,1.938c0.127,0.795,0.085,1.741-0.123,2.812c-0.24,1.232-0.628,2.305-1.152,3.183 c-0.482,0.809-1.096,1.48-1.825,2c-0.696,0.494-1.523,0.869-2.458,1.109c-0.906,0.236-1.939,0.355-3.072,0.355h-0.73 c-0.522,0-1.029,0.188-1.427,0.525c-0.399,0.344-0.663,0.814-0.744,1.328l-0.055,0.299l-0.924,5.855l-0.042,0.215 c-0.011,0.068-0.03,0.102-0.058,0.125c-0.025,0.021-0.061,0.035-0.096,0.035H7.266z"/>
<path fill="#179BD7" d="M23.048,7.667L23.048,7.667L23.048,7.667c-0.028,0.179-0.06,0.362-0.096,0.55 c-1.237,6.351-5.469,8.545-10.874,8.545H9.326c-0.661,0-1.218,0.48-1.321,1.132l0,0l0,0L6.596,26.83l-0.399,2.533 c-0.067,0.428,0.263,0.814,0.695,0.814h4.881c0.578,0,1.069-0.42,1.16-0.99l0.048-0.248l0.919-5.832l0.059-0.32 c0.09-0.572,0.582-0.992,1.16-0.992h0.73c4.729,0,8.431-1.92,9.513-7.476c0.452-2.321,0.218-4.259-0.978-5.622 C24.022,8.286,23.573,7.945,23.048,7.667z"/>
<path fill="#222D65" d="M21.754,7.151c-0.189-0.055-0.384-0.105-0.584-0.15c-0.201-0.044-0.407-0.083-0.619-0.117 c-0.742-0.12-1.555-0.177-2.426-0.177h-7.352c-0.181,0-0.353,0.041-0.507,0.115C9.927,6.985,9.675,7.306,9.614,7.699L8.05,17.605 l-0.045,0.289c0.103-0.652,0.66-1.132,1.321-1.132h2.752c5.405,0,9.637-2.195,10.874-8.545c0.037-0.188,0.068-0.371,0.096-0.55 c-0.313-0.166-0.652-0.308-1.017-0.429C21.941,7.208,21.848,7.179,21.754,7.151z"/>
<path fill="#253B80" d="M9.614,7.699c0.061-0.393,0.313-0.714,0.652-0.876c0.155-0.074,0.326-0.115,0.507-0.115h7.352 c0.871,0,1.684,0.057,2.426,0.177c0.212,0.034,0.418,0.073,0.619,0.117c0.2,0.045,0.395,0.095,0.584,0.15 c0.094,0.028,0.187,0.057,0.278,0.086c0.365,0.121,0.704,0.264,1.017,0.429c0.368-2.347-0.003-3.945-1.272-5.392 C20.378,0.682,17.853,0,14.622,0h-9.38c-0.66,0-1.223,0.48-1.325,1.133L0.01,25.898c-0.077,0.49,0.301,0.932,0.795,0.932h5.791 l1.454-9.225L9.614,7.699z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

@@ -0,0 +1,6 @@
{% load i18n %}
<p>{% blocktrans trimmed %}
The total amount listed above will be withdrawn from your PayPal account after the
confirmation of your purchase.
{% endblocktrans %}</p>
@@ -0,0 +1,6 @@
{% load i18n %}
<p>{% blocktrans trimmed %}
After you clicked continue, we will redirect you to PayPal to fill in your payment
details. You will then be redirected back here to review and confirm your order.
{% endblocktrans %}</p>
@@ -0,0 +1,18 @@
{% load i18n %}
{% if payment_info %}
<dl class="dl-horizontal">
<dt>{% trans "Payment ID" %}</dt>
<dd>{{ payment_info.id }}</dd>
<dt>{% trans "Sale ID" %}</dt>
<dd>{{ sale_id|default_if_none:"?" }}</dd>
<dt>{% trans "Payer" %}</dt>
<dd>{{ payment_info.payer.payer_info.email }}</dd>
<dt>{% trans "Last update" %}</dt>
<dd>{{ payment_info.update_time }}</dd>
<dt>{% trans "Total value" %}</dt>
<dd>{{ payment_info.transactions.0.amount.total }}</dd>
<dt>{% trans "Currency" %}</dt>
<dd>{{ payment_info.transactions.0.amount.currency }}</dd>
</dl>
{% endif %}
@@ -0,0 +1,12 @@
{% load i18n %}
{% if retry %}
<p>{% blocktrans trimmed %}
Our attempt to execute your Payment via PayPal has failed. Please try again or contact us.
{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans trimmed %}
We're waiting for an answer from PayPal regarding your payment. Please contact us, if this
takes more than a few hours.
{% endblocktrans %}</p>
{% endif %}
+39
View File
@@ -0,0 +1,39 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.urls import include, re_path
from .views import abort, oauth_disconnect, success
event_patterns = [
re_path(r'^paypal/', include([
re_path(r'^abort/$', abort, name='abort'),
re_path(r'^return/$', success, name='return'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
])),
]
urlpatterns = [
re_path(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/paypal/disconnect/',
oauth_disconnect, name='oauth.disconnect'),
]
+249
View File
@@ -0,0 +1,249 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
#
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
#
# This file contains Apache-licensed contributions copyrighted by: Flavia Bastos
#
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import json
import logging
from decimal import Decimal
import paypalrestsdk
import paypalrestsdk.exceptions
from django.contrib import messages
from django.db.models import Sum
from django.http import HttpResponse
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django_scopes import scopes_disabled
from pretix.base.models import Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import PaymentException
from pretix.control.permissions import event_permission_required
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.paypal.models import ReferencedPayPalObject
from pretix.plugins.paypal.payment import Paypal
logger = logging.getLogger('pretix.plugins.paypal')
def success(request, *args, **kwargs):
pid = request.GET.get('paymentId')
token = request.GET.get('token')
payer = request.GET.get('PayerID')
request.session['payment_paypal_token'] = token
request.session['payment_paypal_payer'] = payer
urlkwargs = {}
if 'cart_namespace' in kwargs:
urlkwargs['cart_namespace'] = kwargs['cart_namespace']
if request.session.get('payment_paypal_payment'):
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
else:
payment = None
if pid == request.session.get('payment_paypal_id', None):
if payment:
prov = Paypal(request.event)
try:
resp = prov.execute_payment(request, payment)
except PaymentException as e:
messages.error(request, str(e))
urlkwargs['step'] = 'payment'
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
if resp:
return resp
else:
messages.error(request, _('Invalid response from PayPal received.'))
logger.error('Session did not contain payment_paypal_id')
urlkwargs['step'] = 'payment'
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
if payment:
return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
'order': payment.order.code,
'secret': payment.order.secret
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
else:
urlkwargs['step'] = 'confirm'
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs=urlkwargs))
def abort(request, *args, **kwargs):
messages.error(request, _('It looks like you canceled the PayPal payment'))
if request.session.get('payment_paypal_payment'):
payment = OrderPayment.objects.get(pk=request.session.get('payment_paypal_payment'))
else:
payment = None
if payment:
return redirect_to_url(eventreverse(request.event, 'presale:event.order', kwargs={
'order': payment.order.code,
'secret': payment.order.secret
}) + ('?paid=yes' if payment.order.status == Order.STATUS_PAID else ''))
else:
return redirect_to_url(eventreverse(request.event, 'presale:event.checkout', kwargs={'step': 'payment'}))
@csrf_exempt
@require_POST
@scopes_disabled()
def webhook(request, *args, **kwargs):
event_body = request.body.decode('utf-8').strip()
event_json = json.loads(event_body)
# We do not check the signature, we just use it as a trigger to look the charge up.
if 'resource_type' not in event_json:
return HttpResponse("Invalid body, no resource_type given", status=400)
if event_json['resource_type'] not in ('sale', 'refund'):
return HttpResponse("Not interested in this resource type", status=200)
if event_json['resource_type'] == 'sale':
saleid = event_json['resource']['id']
else:
saleid = event_json['resource']['sale_id']
try:
refs = [saleid]
if event_json['resource'].get('parent_payment'):
refs.append(event_json['resource'].get('parent_payment'))
rso = ReferencedPayPalObject.objects.select_related('order', 'order__event').get(
reference__in=refs
)
event = rso.order.event
except ReferencedPayPalObject.DoesNotExist:
rso = None
if hasattr(request, 'event'):
event = request.event
else:
return HttpResponse("Unable to detect event", status=200)
prov = Paypal(event)
prov.init_api()
try:
sale = paypalrestsdk.Sale.find(saleid)
except paypalrestsdk.exceptions.ConnectionError:
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
return HttpResponse('Sale not found', status=500)
if rso and rso.payment:
payment = rso.payment
else:
payments = OrderPayment.objects.filter(order__event=event, provider='paypal',
info__icontains=sale['id'])
payment = None
for p in payments:
payment_info = p.info_data
for res in payment_info['transactions'][0]['related_resources']:
for k, v in res.items():
if k == 'sale' and v['id'] == sale['id']:
payment = p
break
if not payment:
return HttpResponse('Payment not found', status=200)
payment.order.log_action('pretix.plugins.paypal.event', data=event_json)
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED and sale['state'] in ('partially_refunded', 'refunded'):
if event_json['resource_type'] == 'refund':
try:
refund = paypalrestsdk.Refund.find(event_json['resource']['id'])
except paypalrestsdk.exceptions.ConnectionError:
logger.exception('PayPal error on webhook. Event data: %s' % str(event_json))
return HttpResponse('Refund not found', status=500)
known_refunds = {r.info_data.get('id'): r for r in payment.refunds.all()}
if refund['id'] not in known_refunds:
payment.create_external_refund(
amount=abs(Decimal(refund['amount']['total'])),
info=json.dumps(refund.to_dict() if not isinstance(refund, dict) else refund)
)
elif known_refunds.get(refund['id']).state in (
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_STATE_TRANSIT) and refund['state'] == 'completed':
known_refunds.get(refund['id']).done()
if 'total_refunded_amount' in refund:
known_sum = payment.refunds.filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
total_refunded_amount = Decimal(refund['total_refunded_amount']['value'])
if known_sum < total_refunded_amount:
payment.create_external_refund(
amount=total_refunded_amount - known_sum
)
elif sale['state'] == 'refunded':
known_sum = payment.refunds.filter(
state__in=(OrderRefund.REFUND_STATE_DONE, OrderRefund.REFUND_STATE_TRANSIT,
OrderRefund.REFUND_STATE_CREATED, OrderRefund.REFUND_SOURCE_EXTERNAL)
).aggregate(s=Sum('amount'))['s'] or Decimal('0.00')
if known_sum < payment.amount:
payment.create_external_refund(
amount=payment.amount - known_sum
)
elif payment.state in (OrderPayment.PAYMENT_STATE_PENDING, OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_CANCELED, OrderPayment.PAYMENT_STATE_FAILED) and sale['state'] == 'completed':
try:
payment.confirm()
except Quota.QuotaExceededException:
pass
return HttpResponse(status=200)
@event_permission_required('event.settings.general:write')
@require_POST
def oauth_disconnect(request, **kwargs):
del request.event.settings.payment_paypal_connect_refresh_token
del request.event.settings.payment_paypal_connect_user_id
request.event.settings.payment_paypal__enabled = False
messages.success(request, _('Your PayPal account has been disconnected.'))
# Migrate User to PayPal v2
event = request.event
event.disable_plugin("pretix.plugins.paypal")
event.enable_plugin("pretix.plugins.paypal2")
event.save()
return redirect_to_url(reverse('control:event.settings.payment.provider', kwargs={
'organizer': request.event.organizer.slug,
'event': request.event.slug,
'provider': 'paypal_settings'
}))
@@ -1,54 +0,0 @@
# Generated by Django 5.2.17 on 2026-09-08 08:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("pretixbase", "0310_question_valid_string_length_min"),
]
operations = [
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.CreateModel(
name="ReferencedPayPalObject",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
(
"reference",
models.CharField(db_index=True, max_length=190, unique=True),
),
(
"order",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.order",
),
),
(
"payment",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="pretixbase.orderpayment",
),
),
],
options={
"db_table": "paypal_referencedpaypalobject",
},
),
],
database_operations=[]
)
]
+1 -1
View File
@@ -68,7 +68,7 @@ from pretix.plugins.paypal2.client.core.paypal_http_client import (
from pretix.plugins.paypal2.client.customer.partner_referral_create_request import ( from pretix.plugins.paypal2.client.customer.partner_referral_create_request import (
PartnerReferralCreateRequest, PartnerReferralCreateRequest,
) )
from pretix.plugins.paypal2.models import ReferencedPayPalObject from pretix.plugins.paypal.models import ReferencedPayPalObject
logger = logging.getLogger('pretix.plugins.paypal2') logger = logging.getLogger('pretix.plugins.paypal2')
@@ -1,350 +1,350 @@
/* global paypal_client_id, paypal_loadingmessage, gettext */ /*global $, paypal_client_id, paypal_loadingmessage, gettext */
'use strict' 'use strict';
var pretixpaypal = { var pretixpaypal = {
paypal: null, paypal: null,
client_id: null, client_id: null,
order_id: null, order_id: null,
payer_id: null, payer_id: null,
merchant_id: null, merchant_id: null,
currency: null, currency: null,
method: null, method: null,
additional_disabled_funding: null, additional_disabled_funding: null,
additional_enabled_funding: null, additional_enabled_funding: null,
debug_buyer_country: null, debug_buyer_country: null,
continue_button: null, continue_button: null,
paypage: false, paypage: false,
method_map: { method_map: {
wallet: { wallet: {
method: 'wallet', method: 'wallet',
funding_source: 'paypal', funding_source: 'paypal',
// disable_funding: null, //disable_funding: null,
// enable_funding: 'paylater', //enable_funding: 'paylater',
early_auth: true, early_auth: true,
}, },
apm: { apm: {
method: 'apm', method: 'apm',
funding_source: null, funding_source: null,
// disable_funding: null, //disable_funding: null,
// enable_funding: null, //enable_funding: null,
early_auth: false, early_auth: false,
} }
}, },
apm_map: { apm_map: {
paypal: gettext('PayPal'), paypal: gettext('PayPal'),
venmo: gettext('Venmo'), venmo: gettext('Venmo'),
applepay: gettext('Apple Pay'), applepay: gettext('Apple Pay'),
itau: gettext('Itaú'), itau: gettext('Itaú'),
credit: gettext('PayPal Credit'), credit: gettext('PayPal Credit'),
card: gettext('Credit Card'), card: gettext('Credit Card'),
paylater: gettext('PayPal Pay Later'), paylater: gettext('PayPal Pay Later'),
ideal: gettext('iDEAL | Wero'), ideal: gettext('iDEAL | Wero'),
sepa: gettext('SEPA Direct Debit'), sepa: gettext('SEPA Direct Debit'),
bancontact: gettext('Bancontact'), bancontact: gettext('Bancontact'),
giropay: gettext('giropay'), giropay: gettext('giropay'),
sofort: gettext('SOFORT'), sofort: gettext('SOFORT'),
eps: gettext('eps'), eps: gettext('eps'),
mybank: gettext('MyBank'), mybank: gettext('MyBank'),
p24: gettext('Przelewy24'), p24: gettext('Przelewy24'),
verkkopankki: gettext('Verkkopankki'), verkkopankki: gettext('Verkkopankki'),
payu: gettext('PayU'), payu: gettext('PayU'),
blik: gettext('BLIK'), blik: gettext('BLIK'),
trustly: gettext('Trustly'), trustly: gettext('Trustly'),
zimpler: gettext('Zimpler'), zimpler: gettext('Zimpler'),
maxima: gettext('Maxima'), maxima: gettext('Maxima'),
oxxo: gettext('OXXO'), oxxo: gettext('OXXO'),
boleto: gettext('Boleto'), boleto: gettext('Boleto'),
wechatpay: gettext('WeChat Pay'), wechatpay: gettext('WeChat Pay'),
mercadopago: gettext('Mercado Pago') mercadopago: gettext('Mercado Pago')
}, },
readyToSubmitApproval: false, readyToSubmitApproval: false,
load: function () { load: function () {
if (pretixpaypal.paypal === null) { if (pretixpaypal.paypal === null) {
pretixpaypal.client_id = $.trim($('#paypal_client_id').html()) pretixpaypal.client_id = $.trim($("#paypal_client_id").html());
pretixpaypal.merchant_id = $.trim($('#paypal_merchant_id').html()) pretixpaypal.merchant_id = $.trim($("#paypal_merchant_id").html());
pretixpaypal.debug_buyer_country = $.trim($('#paypal_buyer_country').html()) pretixpaypal.debug_buyer_country = $.trim($("#paypal_buyer_country").html());
pretixpaypal.continue_button = $('.checkout-button-row').closest('form').find('.checkout-button-row .btn-primary') pretixpaypal.continue_button = $('.checkout-button-row').closest("form").find(".checkout-button-row .btn-primary");
pretixpaypal.continue_button.closest('div').append('<div id="paypal-button-container"></div>') pretixpaypal.continue_button.closest('div').append('<div id="paypal-button-container"></div>');
pretixpaypal.additional_disabled_funding = $.trim($('#paypal_disable_funding').html()) pretixpaypal.additional_disabled_funding = $.trim($("#paypal_disable_funding").html());
pretixpaypal.additional_enabled_funding = $.trim($('#paypal_enable_funding').html()) pretixpaypal.additional_enabled_funding = $.trim($("#paypal_enable_funding").html());
pretixpaypal.paypage = Boolean($('#paypal-button-container').data('paypage')) pretixpaypal.paypage = Boolean($('#paypal-button-container').data('paypage'));
pretixpaypal.order_id = $.trim($('#paypal_oid').html()) pretixpaypal.order_id = $.trim($("#paypal_oid").html());
pretixpaypal.currency = $('body').attr('data-currency') pretixpaypal.currency = $("body").attr("data-currency");
pretixpaypal.locale = this.guessLocale() pretixpaypal.locale = this.guessLocale();
} }
$('input[name=payment][value^=\'paypal\']').change(function () { $("input[name=payment][value^='paypal']").change(function () {
if (pretixpaypal.paypal !== null) { if (pretixpaypal.paypal !== null) {
pretixpaypal.renderButton($(this).val()) pretixpaypal.renderButton($(this).val());
} else { } else {
pretixpaypal.continue_button.prop('disabled', true) pretixpaypal.continue_button.prop("disabled", true);
} }
}) });
$('input[name=payment]').not('[value^=\'paypal\']').change(function () { $("input[name=payment]").not("[value^='paypal']").change(function () {
pretixpaypal.restore() pretixpaypal.restore();
}) });
// If paypal is pre-selected, we must disable the continue button and handle it after SDK is loaded // If paypal is pre-selected, we must disable the continue button and handle it after SDK is loaded
if ($('input[name=payment][value^=\'paypal\']').is(':checked')) { if ($("input[name=payment][value^='paypal']").is(':checked')) {
pretixpaypal.continue_button.prop('disabled', true) pretixpaypal.continue_button.prop("disabled", true);
} }
// We are setting the cogwheel already here, as the renderAPM() method might take some time to get loaded. // We are setting the cogwheel already here, as the renderAPM() method might take some time to get loaded.
const apmtextselector = $('input[name=payment][value=paypal_apm]').closest('label').find('.accordion-label-text') const apmtextselector = $("input[name=payment][value=paypal_apm]").closest("label").find(".accordion-label-text");
apmtextselector.append(' <span aria-hidden="true" class="fa fa-cog fa-spin"></span>') apmtextselector.append(' <span aria-hidden="true" class="fa fa-cog fa-spin"></span>');
let sdk_url = 'https://www.paypal.com/sdk/js' let sdk_url = 'https://www.paypal.com/sdk/js' +
+ '?client-id=' + pretixpaypal.client_id '?client-id=' + pretixpaypal.client_id +
+ '&components=buttons,funding-eligibility' '&components=buttons,funding-eligibility' +
+ '&currency=' + pretixpaypal.currency '&currency=' + pretixpaypal.currency;
if (pretixpaypal.locale) { if (pretixpaypal.locale) {
sdk_url += '&locale=' + pretixpaypal.locale sdk_url += '&locale=' + pretixpaypal.locale;
} }
if (pretixpaypal.merchant_id) { if (pretixpaypal.merchant_id) {
sdk_url += '&merchant-id=' + pretixpaypal.merchant_id sdk_url += '&merchant-id=' + pretixpaypal.merchant_id;
} }
if (pretixpaypal.additional_disabled_funding) { if (pretixpaypal.additional_disabled_funding) {
sdk_url += '&disable-funding=' + [pretixpaypal.additional_disabled_funding].filter(Boolean).join(',') sdk_url += '&disable-funding=' + [pretixpaypal.additional_disabled_funding].filter(Boolean).join(',');
} }
if (pretixpaypal.additional_enabled_funding) { if (pretixpaypal.additional_enabled_funding) {
sdk_url += '&enable-funding=' + [pretixpaypal.additional_enabled_funding].filter(Boolean).join(',') sdk_url += '&enable-funding=' + [pretixpaypal.additional_enabled_funding].filter(Boolean).join(',');
} }
if (pretixpaypal.debug_buyer_country) { if (pretixpaypal.debug_buyer_country) {
sdk_url += '&buyer-country=' + pretixpaypal.debug_buyer_country sdk_url += '&buyer-country=' + pretixpaypal.debug_buyer_country;
} }
let ppscript = document.createElement('script') let ppscript = document.createElement('script');
let ready = false let ready = false;
let head = document.getElementsByTagName('head')[0] let head = document.getElementsByTagName("head")[0];
ppscript.setAttribute('src', sdk_url) ppscript.setAttribute('src', sdk_url);
ppscript.setAttribute('data-csp-nonce', $.trim($('#csp_nonce').html())) ppscript.setAttribute('data-csp-nonce', $.trim($("#csp_nonce").html()));
ppscript.setAttribute('data-page-type', 'checkout') ppscript.setAttribute('data-page-type', 'checkout');
ppscript.setAttribute('data-partner-attribution-id', 'ramiioGmbH_Cart_PPCP') ppscript.setAttribute('data-partner-attribution-id', 'ramiioGmbH_Cart_PPCP');
document.head.appendChild(ppscript) document.head.appendChild(ppscript);
ppscript.onload = ppscript.onreadystatechange = function () { ppscript.onload = ppscript.onreadystatechange = function () {
if (!ready && (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')) { if (!ready && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) {
ready = true ready = true;
pretixpaypal.paypal = paypal pretixpaypal.paypal = paypal;
// Handle memory leak in IE // Handle memory leak in IE
ppscript.onload = ppscript.onreadystatechange = null ppscript.onload = ppscript.onreadystatechange = null;
if (head && ppscript.parentNode) { if (head && ppscript.parentNode) {
head.removeChild(ppscript) head.removeChild(ppscript);
} }
} }
} };
document.addEventListener('visibilitychange', this.onApproveSubmit) document.addEventListener("visibilitychange", this.onApproveSubmit);
}, },
ready: function () { ready: function () {
if ($('input[name=payment][value=paypal_apm]').length > 0) { if ($("input[name=payment][value=paypal_apm]").length > 0) {
pretixpaypal.renderAPMs() pretixpaypal.renderAPMs();
} }
if ($('input[name=payment][value^=\'paypal\']').is(':checked')) { if ($("input[name=payment][value^='paypal']").is(':checked')) {
pretixpaypal.renderButton($('input[name=payment][value^=\'paypal\']:checked').val()) pretixpaypal.renderButton($("input[name=payment][value^='paypal']:checked").val());
} else if ($('.payment-redo-form').length) { } else if ($(".payment-redo-form").length) {
pretixpaypal.renderButton($('input[name=payment][value^=\'paypal\']').val()) pretixpaypal.renderButton($("input[name=payment][value^='paypal']").val());
} else if ($('#paypal-button-container').data('paypage')) { } else if ($('#paypal-button-container').data('paypage')) {
pretixpaypal.renderButton('paypal_apm') pretixpaypal.renderButton('paypal_apm');
} }
}, },
restore: function () { restore: function () {
// if PayPal has not been initialized, there shouldn't be anything to cleanup // if PayPal has not been initialized, there shouldn't be anything to cleanup
if (pretixpaypal.paypal !== null) { if (pretixpaypal.paypal !== null) {
$('#paypal-button-container').empty() $('#paypal-button-container').empty()
pretixpaypal.continue_button.text(gettext('Continue')) pretixpaypal.continue_button.text(gettext('Continue'));
pretixpaypal.continue_button.show() pretixpaypal.continue_button.show();
} }
pretixpaypal.continue_button.prop('disabled', false) pretixpaypal.continue_button.prop("disabled", false);
}, },
renderButton: function (method) { renderButton: function (method) {
if (method === 'paypal') { if (method === 'paypal') {
method = 'wallet' method = "wallet"
} else { } else {
method = method.split('paypal_').at(-1) method = method.split('paypal_').at(-1)
} }
pretixpaypal.method = pretixpaypal.method_map[method] pretixpaypal.method = pretixpaypal.method_map[method];
if (pretixpaypal.method.method === 'apm' && !pretixpaypal.paypage) { if (pretixpaypal.method.method === 'apm' && !pretixpaypal.paypage) {
pretixpaypal.restore() pretixpaypal.restore();
return return;
} }
$('#paypal-button-container').empty() $('#paypal-button-container').empty()
$('#paypal-card-container').empty() $('#paypal-card-container').empty()
let button = pretixpaypal.paypal.Buttons({ let button = pretixpaypal.paypal.Buttons({
fundingSource: pretixpaypal.method.funding_source, fundingSource: pretixpaypal.method.funding_source,
style: { style: {
layout: pretixpaypal.method.early_auth ? 'horizontal' : 'vertical', layout: pretixpaypal.method.early_auth ? 'horizontal' : 'vertical',
// color: 'white', //color: 'white',
shape: 'rect', shape: 'rect',
label: 'pay', label: 'pay',
tagline: false tagline: false
}, },
createOrder: function (data, actions) { createOrder: function (data, actions) {
if (pretixpaypal.order_id) { if (pretixpaypal.order_id) {
return pretixpaypal.order_id return pretixpaypal.order_id;
} }
// On the paypal:pay view, we already pregenerated the OID. // On the paypal:pay view, we already pregenerated the OID.
// Since this view is also only used for APMs, we only need the XHR-calls for the Smart Payment Buttons. // Since this view is also only used for APMs, we only need the XHR-calls for the Smart Payment Buttons.
if (pretixpaypal.paypage) { if (pretixpaypal.paypage) {
return $('#payment_paypal_' + pretixpaypal.method.method + '_oid') return $("#payment_paypal_" + pretixpaypal.method.method + "_oid");
} else { } else {
var xhrurl = $('#payment_paypal_' + pretixpaypal.method.method + '_xhr').val() var xhrurl = $("#payment_paypal_" + pretixpaypal.method.method + "_xhr").val();
} }
return fetch(xhrurl, { return fetch(xhrurl, {
method: 'POST' method: 'POST'
}).then(function (res) { }).then(function (res) {
return res.json() return res.json();
}).then(function (data) { }).then(function (data) {
if ('id' in data) { if ('id' in data) {
return data.id return data.id;
} else { } else {
// Refreshing the page to surface the request-error message // Refreshing the page to surface the request-error message
location.reload() location.reload();
} }
}) });
}, },
onApprove: function (data, actions) { onApprove: function (data, actions) {
waitingDialog.show(gettext('Confirming your payment …')) waitingDialog.show(gettext("Confirming your payment …"));
pretixpaypal.order_id = data.orderID pretixpaypal.order_id = data.orderID;
pretixpaypal.payer_id = data.payerID pretixpaypal.payer_id = data.payerID;
let method = pretixpaypal.paypage ? 'wallet' : pretixpaypal.method.method let method = pretixpaypal.paypage ? "wallet" : pretixpaypal.method.method;
let selectorstub = '#payment_paypal_' + method let selectorstub = "#payment_paypal_" + method;
// Insert the tokens into the form, so it gets submitted to the server // Insert the tokens into the form, so it gets submitted to the server
$(selectorstub + '_oid').val(pretixpaypal.order_id) $(selectorstub + "_oid").val(pretixpaypal.order_id);
$(selectorstub + '_payer').val(pretixpaypal.payer_id) $(selectorstub + "_payer").val(pretixpaypal.payer_id);
// We are moving the submission to a separate function, which is also an EventListener, since // We are moving the submission to a separate function, which is also an EventListener, since
// SFSafariView refuses to submit a form that is not visible. Unfortunately, that is exactly the case // SFSafariView refuses to submit a form that is not visible. Unfortunately, that is exactly the case
// when the ticket shop is used on iOS within an SFSafariView and the PayPal payment popup has not // when the ticket shop is used on iOS within an SFSafariView and the PayPal payment popup has not
// closed itself quickly enough. // closed itself quickly enough.
pretixpaypal.readyToSubmitApproval = true pretixpaypal.readyToSubmitApproval = true;
pretixpaypal.onApproveSubmit() pretixpaypal.onApproveSubmit();
// billingToken: null // billingToken: null
// facilitatorAccessToken: "A21AAL_fEu0gDD-sIXyOy65a6MjgSJJrhmxuPcxxUGnL5gW2DzTxiiAksfoC4x8hD-BjeY1LsFVKl7ceuO7UR1a9pQr8Q_AVw" // facilitatorAccessToken: "A21AAL_fEu0gDD-sIXyOy65a6MjgSJJrhmxuPcxxUGnL5gW2DzTxiiAksfoC4x8hD-BjeY1LsFVKl7ceuO7UR1a9pQr8Q_AVw"
// orderID: "7RF70259NY7589848" // orderID: "7RF70259NY7589848"
// payerID: "8M3BU92Z97VXA" // payerID: "8M3BU92Z97VXA"
// paymentID: null // paymentID: null
}, },
}) });
if (button.isEligible()) { if (button.isEligible()) {
button.render('#paypal-button-container') button.render('#paypal-button-container');
pretixpaypal.continue_button.hide() pretixpaypal.continue_button.hide();
} else { } else {
pretixpaypal.continue_button.text(gettext('Payment method unavailable')) pretixpaypal.continue_button.text(gettext('Payment method unavailable'));
pretixpaypal.continue_button.show() pretixpaypal.continue_button.show();
} }
}, },
onApproveSubmit: function () { onApproveSubmit: function() {
if (document.visibilityState === 'visible' && pretixpaypal.readyToSubmitApproval === true) { if (document.visibilityState === "visible" && pretixpaypal.readyToSubmitApproval === true) {
let method = pretixpaypal.paypage ? 'wallet' : pretixpaypal.method.method let method = pretixpaypal.paypage ? "wallet" : pretixpaypal.method.method;
let selectorstub = '#payment_paypal_' + method let selectorstub = "#payment_paypal_" + method;
let $form = $(selectorstub + '_oid').closest('form') var $form = $(selectorstub + "_oid").closest("form");
$form.get(0).submit() $form.get(0).submit();
} }
}, },
renderAPMs: function () { renderAPMs: function () {
pretixpaypal.restore() pretixpaypal.restore();
let inputselector = $('input[name=payment][value=paypal_apm]') let inputselector = $("input[name=payment][value=paypal_apm]");
let textselector = inputselector.closest('label').find('.accordion-label-text') let textselector = inputselector.closest("label").find('.accordion-label-text');
let eligibles = [] let eligibles = [];
pretixpaypal.paypal.getFundingSources().forEach(function (fundingSource) { pretixpaypal.paypal.getFundingSources().forEach(function (fundingSource) {
// Let's always skip PayPal, since it's always a dedicated funding source // Let's always skip PayPal, since it's always a dedicated funding source
if (fundingSource === 'paypal') { if (fundingSource === 'paypal') {
return return;
} }
// This could also be paypal.Marks() - but they only expose images instead of cleartext... // This could also be paypal.Marks() - but they only expose images instead of cleartext...
let button = pretixpaypal.paypal.Buttons({ let button = pretixpaypal.paypal.Buttons({
fundingSource: fundingSource fundingSource: fundingSource
}) });
if (button.isEligible()) { if (button.isEligible()) {
eligibles.push(gettext(pretixpaypal.apm_map[fundingSource] || fundingSource)) eligibles.push(gettext(pretixpaypal.apm_map[fundingSource] || fundingSource));
} }
}) });
inputselector.attr('title', eligibles.join(', ')) inputselector.attr('title', eligibles.join(', '));
textselector.fadeOut(300, function () { textselector.fadeOut(300, function () {
textselector.text(eligibles.join(', ')) textselector.text(eligibles.join(', '));
textselector.fadeIn(300) textselector.fadeIn(300);
}) });
}, },
guessLocale: function () { guessLocale: function() {
// This is a horrible hackjob and does not at all take into consideration the actual locale. // This is a horrible hackjob and does not at all take into consideration the actual locale.
// Instead, we only look at the language that the shop is currently being displayed in and make // Instead, we only look at the language that the shop is currently being displayed in and make
// that into a locale. // that into a locale.
let allowed_locales = [ let allowed_locales = [
'en_US', 'en_US',
'ar_DZ', 'ar_DZ',
'fr_FR', 'fr_FR',
'es_ES', 'es_ES',
'zh_CN', 'zh_CN',
'de_DE', 'de_DE',
'nl_NL', 'nl_NL',
'pt_PT', 'pt_PT',
'cs_CZ', 'cs_CZ',
'da_DK', 'da_DK',
'fi_FI', 'fi_FI',
'el_GR', 'el_GR',
'hu_HU', 'hu_HU',
'id_ID', 'id_ID',
'he_IL', 'he_IL',
'it_IT', 'it_IT',
'ja_JP', 'ja_JP',
'ru_RU', 'ru_RU',
'no_NO', 'no_NO',
'pl_PL', 'pl_PL',
'sk_SK', 'sk_SK',
'sv_SE', 'sv_SE',
'th_TH', 'th_TH',
'tr_TR', 'tr_TR',
] ]
let lang = $('body').attr('data-locale').split('-')[0] let lang = $("body").attr("data-locale").split('-')[0];
return allowed_locales.find(element => element.startsWith(lang)) return allowed_locales.find(element => element.startsWith(lang));
} }
} };
$(function () { $(function () {
// This script is always loaded if paypal is enabled as a payment method, regardless of // This script is always loaded if paypal is enabled as a payment method, regardless of
// whether it is available (it could e.g. be hidden or limited to certain countries). // whether it is available (it could e.g. be hidden or limited to certain countries).
// We do not want to unnecessarily load the sdk. // We do not want to unnecessarily load the sdk.
// If no paypal/paypal_apm payment option is present and we are not on // If no paypal/paypal_apm payment option is present and we are not on
// the (APM) PayView, then we do not need the SDK. // the (APM) PayView, then we do not need the SDK.
if (!$('input[name=payment][value^=\'paypal\']').length && !$('#paypal-button-container').data('paypage')) { if (!$("input[name=payment][value^='paypal']").length && !$('#paypal-button-container').data('paypage')) {
return return
} }
pretixpaypal.load(); pretixpaypal.load();
(async () => { (async() => {
while (!pretixpaypal.paypal) while(!pretixpaypal.paypal)
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 1000));
pretixpaypal.ready() pretixpaypal.ready();
})() })();
}) });
+3 -3
View File
@@ -67,10 +67,10 @@ from pretix.multidomain.urlreverse import eventreverse
from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_request import ( from pretix.plugins.paypal2.client.customer.partners_merchantintegrations_get_request import (
PartnersMerchantIntegrationsGetRequest, PartnersMerchantIntegrationsGetRequest,
) )
from pretix.plugins.paypal2.models import ReferencedPayPalObject
from pretix.plugins.paypal2.payment import ( from pretix.plugins.paypal2.payment import (
PaypalMethod, PaypalMethod as Paypal, PaypalWallet, PaypalMethod, PaypalMethod as Paypal, PaypalWallet,
) )
from pretix.plugins.paypal.models import ReferencedPayPalObject
from pretix.presale.views import get_cart from pretix.presale.views import get_cart
from pretix.presale.views.cart import cart_session from pretix.presale.views.cart import cart_session
@@ -350,8 +350,8 @@ def webhook(request, *args, **kwargs):
return HttpResponse("Invalid body, no event_type given", status=400) return HttpResponse("Invalid body, no event_type given", status=400)
if event_json['event_type'].startswith('PAYMENT.SALE.'): if event_json['event_type'].startswith('PAYMENT.SALE.'):
logger.info(f"Received PPv1 webhook: {json.dumps(event_json)}") from pretix.plugins.paypal.views import webhook
return HttpResponse("PayPal V1 no longer supported", status=400) return webhook(request, *args, **kwargs)
# V1/V2 Sorting -- End # V1/V2 Sorting -- End
# We do not check the signature, we just use it as a trigger to look the charge up. # We do not check the signature, we just use it as a trigger to look the charge up.
+3 -15
View File
@@ -55,8 +55,6 @@ from django_countries.fields import Country
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER from reportlab.lib.enums import TA_CENTER
from reportlab.lib.units import mm from reportlab.lib.units import mm
from reportlab.lib.utils import simpleSplit
from reportlab.pdfbase.pdfmetrics import stringWidth
from reportlab.pdfgen.canvas import Canvas from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import PageBreak, Spacer, Table, TableStyle from reportlab.platypus import PageBreak, Spacer, Table, TableStyle
@@ -228,20 +226,10 @@ class ReportlabExportMixin:
def page_header(self, canvas, doc): def page_header(self, canvas, doc):
from reportlab.lib.units import mm from reportlab.lib.units import mm
font_name = 'OpenSans' canvas.setFont('OpenSans', 10)
font_size = 10 canvas.drawString(doc.leftMargin, self.pagesize[1] - 15 * mm, self.get_left_header_string())
left_string = self.get_left_header_string()
right_string = self.get_right_header_string()
right_width = stringWidth(right_string, font_name, font_size)
max_left_width = self.pagesize[0] - doc.leftMargin - doc.rightMargin - right_width - 5 * mm
left_string_lines = simpleSplit(left_string, font_name, font_size, max_left_width)
if len(left_string_lines) > 1:
left_string = left_string_lines[0] + " …"
canvas.setFont(font_name, font_size)
canvas.drawString(doc.leftMargin, self.pagesize[1] - 15 * mm, left_string)
canvas.drawRightString(self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 15 * mm, canvas.drawRightString(self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 15 * mm,
right_string) self.get_right_header_string())
canvas.setStrokeColorRGB(0, 0, 0) canvas.setStrokeColorRGB(0, 0, 0)
canvas.line(doc.leftMargin, self.pagesize[1] - 17 * mm, canvas.line(doc.leftMargin, self.pagesize[1] - 17 * mm,
self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 17 * mm) self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 17 * mm)
+8 -11
View File
@@ -56,11 +56,18 @@ from pretix.base.services.placeholders import FormPlaceholderMixin # noqa
class BaseMailForm(FormPlaceholderMixin, forms.Form): class BaseMailForm(FormPlaceholderMixin, forms.Form):
subject = forms.CharField(label=_("Subject")) subject = forms.CharField(label=_("Subject"))
message = forms.CharField(label=_("Message")) message = forms.CharField(label=_("Message"))
attachment = CachedFileField(
label=_("Attachment"),
required=False,
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
'of no more than 2 MB in size.'),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
event = self.event = kwargs.pop('event') event = self.event = kwargs.pop('event')
context_parameters = kwargs.pop('context_parameters') context_parameters = kwargs.pop('context_parameters')
request = kwargs.pop('request')
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['subject'] = I18nFormField( self.fields['subject'] = I18nFormField(
label=_('Subject'), label=_('Subject'),
@@ -72,16 +79,6 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
widget=I18nMarkdownTextarea, required=True, widget=I18nMarkdownTextarea, required=True,
locales=event.settings.get('locales'), locales=event.settings.get('locales'),
) )
self.fields['attachment'] = CachedFileField(
label=_("Attachment"),
required=False,
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
help_text=_(
'Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
'of no more than 2 MB in size.'),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT,
request=request,
)
self._set_field_placeholders('subject', context_parameters, rich=False) self._set_field_placeholders('subject', context_parameters, rich=False)
self._set_field_placeholders('message', context_parameters, rich=True) self._set_field_placeholders('message', context_parameters, rich=True)
+18 -26
View File
@@ -157,7 +157,6 @@ class BaseSenderView(EventPermissionRequiredMixin, FormView):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event kwargs['event'] = self.request.event
kwargs['context_parameters'] = self.context_parameters kwargs['context_parameters'] = self.context_parameters
kwargs['request'] = self.request
if 'from_log' in self.request.GET: if 'from_log' in self.request.GET:
try: try:
from_log_id = self.request.GET.get('from_log') from_log_id = self.request.GET.get('from_log')
@@ -355,9 +354,9 @@ class OrderSendView(BaseSenderView):
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True) statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True)
orders = qs.filter(statusq) orders = qs.filter(statusq)
opq = OrderPosition.objects.with_scopes_disabled().filter( opq = OrderPosition.objects.filter(
Q(item_id__in=[i.pk for i in form.cleaned_data.get('items')]) | Q(Exists( Q(item_id__in=[i.pk for i in form.cleaned_data.get('items')]) | Q(Exists(
OrderPosition.objects.with_scopes_disabled().filter( OrderPosition.objects.filter(
addon_to_id=OuterRef('pk'), addon_to_id=OuterRef('pk'),
item_id__in=[i.pk for i in form.cleaned_data.get('items')] item_id__in=[i.pk for i in form.cleaned_data.get('items')]
) )
@@ -367,43 +366,36 @@ class OrderSendView(BaseSenderView):
) )
if form.cleaned_data.get('filter_checkins'): if form.cleaned_data.get('filter_checkins'):
ci_filter = Q(pk__in=[]) # return nothing ql = []
if form.cleaned_data.get('not_checked_in'): if form.cleaned_data.get('not_checked_in'):
consider_tickets_used_lists = list(self.request.event.checkin_lists.filter(consider_tickets_used=True).values_list("id", flat=True))
opq = opq.alias( opq = opq.alias(
any_checkins=Exists( any_checkins=Exists(
Checkin.objects.with_scopes_disabled().filter( Checkin.all.filter(
position_id=OuterRef('pk'), Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
list_id__in=consider_tickets_used_lists, successful=True,
) list__consider_tickets_used=True,
) | Exists(
Checkin.objects.with_scopes_disabled().filter(
position__addon_to_id=OuterRef('pk'),
list_id__in=consider_tickets_used_lists,
) )
) )
) )
ci_filter |= Q(any_checkins=False) ql.append(Q(any_checkins=False))
if form.cleaned_data.get('checkin_lists'): if form.cleaned_data.get('checkin_lists'):
opq = opq.alias( opq = opq.alias(
matching_checkins=Exists( matching_checkins=Exists(
Checkin.objects.with_scopes_disabled().filter( Checkin.all.filter(
position_id=OuterRef('pk'), Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])],
)
) | Exists(
Checkin.objects.with_scopes_disabled().filter(
position__addon_to_id=OuterRef('pk'),
list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])], list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])],
successful=True
) )
) )
) )
ci_filter |= Q(matching_checkins=True) ql.append(Q(matching_checkins=True))
if len(ql) == 2:
opq = opq.filter(ci_filter) opq = opq.filter(ql[0] | ql[1])
elif ql:
opq = opq.filter(ql[0])
else:
opq = opq.none()
if form.cleaned_data.get('subevent'): if form.cleaned_data.get('subevent'):
opq = opq.filter(subevent=form.cleaned_data.get('subevent')) opq = opq.filter(subevent=form.cleaned_data.get('subevent'))
@@ -1,81 +1,81 @@
/* globals Morris, django */ /*globals $, Morris, gettext, django*/
function gettext (msgid) { function gettext(msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') { if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid) return django.gettext(msgid);
} }
return msgid return msgid;
} }
$(function () { $(function () {
$('.chart').css('height', '250px') $(".chart").css("height", "250px");
new Morris.Area({ new Morris.Area({
element: 'obd_chart', element: 'obd_chart',
data: JSON.parse($('#obd-data').html()), data: JSON.parse($("#obd-data").html()),
xkey: 'date', xkey: 'date',
ykeys: ['ordered', 'paid'], ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')], labels: [gettext('Placed orders'), gettext('Paid orders')],
lineColors: ['#3b1c4a', '#50a167'], lineColors: ['#3b1c4a', '#50a167'],
smooth: false, smooth: false,
resize: true, resize: true,
fillOpacity: 0.3, fillOpacity: 0.3,
behaveLikeLine: true behaveLikeLine: true
}) });
new Morris.Area({ new Morris.Area({
element: 'abd_chart', element: 'abd_chart',
data: JSON.parse($('#abd-data').html()), data: JSON.parse($("#abd-data").html()),
xkey: 'date', xkey: 'date',
ykeys: ['ordered', 'paid'], ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')], labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'], lineColors: ['#3b1c4a', '#50a167'],
smooth: false, smooth: false,
resize: true, resize: true,
fillOpacity: 0.3, fillOpacity: 0.3,
behaveLikeLine: true behaveLikeLine: true
}) });
new Morris.Area({ new Morris.Area({
element: 'abt_chart', element: 'abt_chart',
data: JSON.parse($('#abt-data').html()), data: JSON.parse($("#abt-data").html()),
xkey: 'date', xkey: 'date',
ykeys: ['ordered', 'paid'], ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')], labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'], lineColors: ['#3b1c4a', '#50a167'],
smooth: false, smooth: false,
resize: true, resize: true,
fillOpacity: 0.3, fillOpacity: 0.3,
behaveLikeLine: true behaveLikeLine: true
}) });
new Morris.Area({ new Morris.Area({
element: 'rev_chart', element: 'rev_chart',
data: JSON.parse($('#rev-data').html()), data: JSON.parse($("#rev-data").html()),
xkey: 'date', xkey: 'date',
ykeys: ['revenue'], ykeys: ['revenue'],
labels: [gettext('Total revenue')], labels: [gettext('Total revenue')],
smooth: false, smooth: false,
resize: true, resize: true,
lineColors: ['#3b1c4a'], lineColors: ['#3b1c4a'],
fillOpacity: 0.3, fillOpacity: 0.3,
preUnits: $.trim($('#currency').html()) + ' ' preUnits: $.trim($("#currency").html()) + ' '
}) });
new Morris.Bar({ new Morris.Bar({
element: 'obp_chart', element: 'obp_chart',
data: JSON.parse($('#obp-data').html()), data: JSON.parse($("#obp-data").html()),
xkey: 'item_short', xkey: 'item_short',
ykeys: ['ordered', 'paid'], ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')], labels: [gettext('Placed orders'), gettext('Paid orders')],
barColors: ['#3b1c4a', '#50a167'], barColors: ['#3b1c4a', '#50a167'],
hoverCallback: function (index, options, content, row) { hoverCallback: function (index, options, content, row) {
console.log(content) console.log(content);
let $c = $('<div>' + content + '</div>') var $c = $("<div>" + content + "</div>");
let $label = $c.find('.morris-hover-row-label') var $label = $c.find(".morris-hover-row-label");
$label.text(row.item) $label.text(row.item);
let newc = $label.get(0).outerHTML var newc = $label.get(0).outerHTML;
$c.find('.morris-hover-point').each(function (i, r) { $c.find('.morris-hover-point').each(function (i, r) {
if ($.trim($(r).text().split('\n')[2]) !== '0') { if ($.trim($(r).text().split("\n")[2]) !== "0") {
newc += r.outerHTML newc += r.outerHTML;
} }
}) });
return newc return newc;
}, },
resize: true, resize: true,
xLabelAngle: 30 xLabelAngle: 30
}) });
}) });
@@ -1,435 +1,435 @@
/* global stripe_pubkey, stripe_loadingmessage, gettext */ /*global $, stripe_pubkey, stripe_loadingmessage, gettext */
'use strict' 'use strict';
var pretixstripe = { var pretixstripe = {
stripe: null, stripe: null,
elements: null, elements: null,
card: null, card: null,
sepa: null, sepa: null,
affirm: null, affirm: null,
klarna: null, klarna: null,
paymentRequest: null, paymentRequest: null,
paymentRequestButton: null, paymentRequestButton: null,
pm_request: function (method, element, kwargs = {}) { 'pm_request': function (method, element, kwargs = {}) {
waitingDialog.show(gettext('Contacting Stripe …')) waitingDialog.show(gettext("Contacting Stripe …"));
$('.stripe-errors').hide() $(".stripe-errors").hide();
pretixstripe.stripe.createPaymentMethod(method, element, kwargs).then(function (result) { pretixstripe.stripe.createPaymentMethod(method, element, kwargs).then(function (result) {
waitingDialog.hide() waitingDialog.hide();
if (result.error) { if (result.error) {
$('.stripe-errors').stop().hide().removeClass('sr-only') $(".stripe-errors").stop().hide().removeClass("sr-only");
$('.stripe-errors').html('<div class=\'alert alert-danger\'>' + result.error.message + '</div>') $(".stripe-errors").html("<div class='alert alert-danger'>" + result.error.message + "</div>");
$('.stripe-errors').slideDown() $(".stripe-errors").slideDown();
} else { } else {
let $form = $('#stripe_' + method + '_payment_method_id').closest('form') var $form = $("#stripe_" + method + "_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server // Insert the token into the form so it gets submitted to the server
$('#stripe_' + method + '_payment_method_id').val(result.paymentMethod.id) $("#stripe_" + method + "_payment_method_id").val(result.paymentMethod.id);
if (method === 'card') { if (method === 'card') {
$('#stripe_card_brand').val(result.paymentMethod.card.brand) $("#stripe_card_brand").val(result.paymentMethod.card.brand);
$('#stripe_card_last4').val(result.paymentMethod.card.last4) $("#stripe_card_last4").val(result.paymentMethod.card.last4);
} }
if (method === 'sepa_debit') { if (method === 'sepa_debit') {
$('#stripe_sepa_debit_last4').val(result.paymentMethod.sepa_debit.last4) $("#stripe_sepa_debit_last4").val(result.paymentMethod.sepa_debit.last4);
} }
// and submit // and submit
$form.get(0).submit() $form.get(0).submit();
} }
}).catch((e) => { }).catch((e) => {
waitingDialog.hide() waitingDialog.hide();
$('.stripe-errors').stop().hide().removeClass('sr-only') $(".stripe-errors").stop().hide().removeClass("sr-only");
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + e + '</div>') $(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + e + "</div>");
$('.stripe-errors').slideDown() $(".stripe-errors").slideDown();
}) });
}, },
load: function () { 'load': function () {
if (pretixstripe.stripe !== null) { if (pretixstripe.stripe !== null) {
return return;
} }
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', true) $('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", true);
$.ajax( $.ajax(
{ {
url: 'https://js.stripe.com/v3/', url: 'https://js.stripe.com/v3/',
dataType: 'script', dataType: 'script',
success: function () { success: function () {
if ($.trim($('#stripe_connectedAccountId').html())) { if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), { pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($('#stripe_connectedAccountId').html()), stripeAccount: $.trim($("#stripe_connectedAccountId").html()),
locale: $.trim($('body').attr('data-locale')) locale: $.trim($("body").attr("data-locale"))
}) });
} else { } else {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), { pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
locale: $.trim($('body').attr('data-locale')) locale: $.trim($("body").attr("data-locale"))
}) });
} }
pretixstripe.elements = pretixstripe.stripe.elements() pretixstripe.elements = pretixstripe.stripe.elements();
if ($.trim($('#stripe_merchantcountry').html()) !== '') { if ($.trim($("#stripe_merchantcountry").html()) !== "") {
try { try {
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({ pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
country: $('#stripe_merchantcountry').html(), country: $("#stripe_merchantcountry").html(),
currency: $('#stripe_card_currency').val().toLowerCase(), currency: $("#stripe_card_currency").val().toLowerCase(),
total: { total: {
label: gettext('Total'), label: gettext('Total'),
amount: parseInt($('#stripe_card_total').val()) amount: parseInt($("#stripe_card_total").val())
}, },
displayItems: [], displayItems: [],
requestPayerName: false, requestPayerName: false,
requestPayerEmail: false, requestPayerEmail: false,
requestPayerPhone: false, requestPayerPhone: false,
requestShipping: false, requestShipping: false,
}) });
pretixstripe.paymentRequest.on('paymentmethod', function (ev) { pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success') ev.complete('success');
let $form = $('#stripe_card_payment_method_id').closest('form') var $form = $("#stripe_card_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server // Insert the token into the form so it gets submitted to the server
$('#stripe_card_payment_method_id').val(ev.paymentMethod.id) $("#stripe_card_payment_method_id").val(ev.paymentMethod.id);
$('#stripe_card_brand').val(ev.paymentMethod.card.brand) $("#stripe_card_brand").val(ev.paymentMethod.card.brand);
$('#stripe_card_last4').val(ev.paymentMethod.card.last4) $("#stripe_card_last4").val(ev.paymentMethod.card.last4);
// and submit // and submit
$form.get(0).submit() $form.get(0).submit();
}) });
} catch (e) { } catch (e) {
pretixstripe.paymentRequest = null pretixstripe.paymentRequest = null;
} }
} else { } else {
pretixstripe.paymentRequest = null pretixstripe.paymentRequest = null;
} }
if ($('#stripe-card').length) { if ($("#stripe-card").length) {
pretixstripe.card = pretixstripe.elements.create('card', { pretixstripe.card = pretixstripe.elements.create('card', {
style: { 'style': {
base: { 'base': {
fontFamily: '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif', 'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
fontSize: '14px', 'fontSize': '14px',
color: '#555555', 'color': '#555555',
lineHeight: '1.42857', 'lineHeight': '1.42857',
border: '1px solid #ccc', 'border': '1px solid #ccc',
'::placeholder': { '::placeholder': {
color: 'rgba(0,0,0,0.4)', color: 'rgba(0,0,0,0.4)',
}, },
}, },
invalid: { 'invalid': {
color: 'red', 'color': 'red',
}, },
}, },
classes: { classes: {
focus: 'is-focused', focus: 'is-focused',
invalid: 'has-error', invalid: 'has-error',
} }
}) });
pretixstripe.card.mount('#stripe-card') pretixstripe.card.mount("#stripe-card");
pretixstripe.card.on('ready', function () { pretixstripe.card.on('ready', function () {
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', false) $('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
}) });
} }
if ($('#stripe-sepa').length) { if ($("#stripe-sepa").length) {
pretixstripe.sepa = pretixstripe.elements.create('iban', { pretixstripe.sepa = pretixstripe.elements.create('iban', {
style: { 'style': {
base: { 'base': {
fontFamily: '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif', 'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
fontSize: '14px', 'fontSize': '14px',
color: '#555555', 'color': '#555555',
lineHeight: '1.42857', 'lineHeight': '1.42857',
border: '1px solid #ccc', 'border': '1px solid #ccc',
'::placeholder': { '::placeholder': {
color: 'rgba(0,0,0,0.4)', color: 'rgba(0,0,0,0.4)',
}, },
}, },
invalid: { 'invalid': {
color: 'red', 'color': 'red',
}, },
}, },
supportedCountries: ['SEPA'], supportedCountries: ['SEPA'],
classes: { classes: {
focus: 'is-focused', focus: 'is-focused',
invalid: 'has-error', invalid: 'has-error',
} }
}) });
pretixstripe.sepa.on('change', function (event) { pretixstripe.sepa.on('change', function (event) {
// List of IBAN-countries, that require the country as well as line1-property according to // List of IBAN-countries, that require the country as well as line1-property according to
// https://stripe.com/docs/payments/sepa-debit/accept-a-payment?platform=web&ui=element#web-submit-payment // https://stripe.com/docs/payments/sepa-debit/accept-a-payment?platform=web&ui=element#web-submit-payment
if (['AD', 'PF', 'TF', 'GI', 'GB', 'GG', 'VA', 'IM', 'JE', 'MC', 'NC', 'BL', 'PM', 'SM', 'CH', 'WF'].indexOf(event.country) > 0) { if (['AD', 'PF', 'TF', 'GI', 'GB', 'GG', 'VA', 'IM', 'JE', 'MC', 'NC', 'BL', 'PM', 'SM', 'CH', 'WF'].indexOf(event.country) > 0) {
$('#stripe_sepa_debit_country').prop('checked', true) $("#stripe_sepa_debit_country").prop('checked', true);
$('#stripe_sepa_debit_country').change() $("#stripe_sepa_debit_country").change();
} else { } else {
$('#stripe_sepa_debit_country').prop('checked', false) $("#stripe_sepa_debit_country").prop('checked', false);
$('#stripe_sepa_debit_country').change() $("#stripe_sepa_debit_country").change();
} }
if (event.bankName) { if (event.bankName) {
$('#stripe_sepa_debit_bank').val(event.bankName) $("#stripe_sepa_debit_bank").val(event.bankName);
} }
}) });
pretixstripe.sepa.mount('#stripe-sepa') pretixstripe.sepa.mount("#stripe-sepa");
pretixstripe.sepa.on('ready', function () { pretixstripe.sepa.on('ready', function () {
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', false) $('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
}) });
} }
if ($('#stripe-affirm').length) { if ($("#stripe-affirm").length) {
pretixstripe.affirm = pretixstripe.elements.create('affirmMessage', { pretixstripe.affirm = pretixstripe.elements.create('affirmMessage', {
amount: parseInt($('#stripe_affirm_total').val()), 'amount': parseInt($("#stripe_affirm_total").val()),
currency: $('#stripe_affirm_currency').val(), 'currency': $("#stripe_affirm_currency").val(),
}) });
pretixstripe.affirm.mount('#stripe-affirm') pretixstripe.affirm.mount('#stripe-affirm');
} }
if ($('#stripe-klarna').length) { if ($("#stripe-klarna").length) {
try { try {
pretixstripe.klarna = pretixstripe.elements.create('paymentMethodMessaging', { pretixstripe.klarna = pretixstripe.elements.create('paymentMethodMessaging', {
amount: parseInt($('#stripe_klarna_total').val()), 'amount': parseInt($("#stripe_klarna_total").val()),
currency: $('#stripe_klarna_currency').val(), 'currency': $("#stripe_klarna_currency").val(),
countryCode: $('#stripe_klarna_country').val(), 'countryCode': $("#stripe_klarna_country").val(),
paymentMethodTypes: ['klarna'], 'paymentMethodTypes': ['klarna'],
}) });
pretixstripe.klarna.mount('#stripe-klarna') pretixstripe.klarna.mount('#stripe-klarna');
} catch (e) { } catch (e) {
console.error(e) console.error(e);
$('#stripe-klarna').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + e + '</div>') $("#stripe-klarna").html("<div class='alert alert-danger'>Technical error, please contact support: " + e + "</div>");
} }
} }
if ($('#stripe-payment-request-button').length && pretixstripe.paymentRequest != null) { if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', { pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest, paymentRequest: pretixstripe.paymentRequest,
}) });
pretixstripe.paymentRequest.canMakePayment().then(function (result) { pretixstripe.paymentRequest.canMakePayment().then(function (result) {
if (result) { if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button') pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button');
$('#stripe-card-elements .stripe-or').removeClass('hidden') $('#stripe-card-elements .stripe-or').removeClass("hidden");
$('#stripe-payment-request-button').parent().removeClass('hidden') $('#stripe-payment-request-button').parent().removeClass("hidden");
} else { } else {
$('#stripe-payment-request-button').hide() $('#stripe-payment-request-button').hide();
document.getElementById('stripe-payment-request-button').style.display = 'none' document.getElementById('stripe-payment-request-button').style.display = 'none';
} }
}) });
} }
} }
} }
) );
}, },
withStripe: function (callback) { 'withStripe': function (callback) {
$.ajax({ $.ajax({
url: 'https://js.stripe.com/v3/', url: 'https://js.stripe.com/v3/',
dataType: 'script', dataType: 'script',
success: function () { success: function () {
if ($.trim($('#stripe_connectedAccountId').html())) { if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), { pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($('#stripe_connectedAccountId').html()), stripeAccount: $.trim($("#stripe_connectedAccountId").html()),
locale: $.trim($('body').attr('data-locale')) locale: $.trim($("body").attr("data-locale"))
}) });
} else { } else {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), { pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
locale: $.trim($('body').attr('data-locale')) locale: $.trim($("body").attr("data-locale"))
}) });
} }
callback() callback();
} }
}) });
}, },
handleAlipayAction: function (payment_intent_client_secret) { 'handleAlipayAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () { pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmAlipayPayment( pretixstripe.stripe.confirmAlipayPayment(
payment_intent_client_secret, payment_intent_client_secret,
{ {
return_url: window.location.href return_url: window.location.href
} }
).then(function (result) { ).then(function (result) {
if (result.error) { if (result.error) {
waitingDialog.hide() waitingDialog.hide();
$('.stripe-errors').stop().hide().removeClass('sr-only') $(".stripe-errors").stop().hide().removeClass("sr-only");
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>') $(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$('.stripe-errors').slideDown() $(".stripe-errors").slideDown();
} else { } else {
waitingDialog.show(gettext('Confirming your payment …')) waitingDialog.show(gettext("Confirming your payment …"));
} }
}) });
}) });
}, },
handleWechatAction: function (payment_intent_client_secret) { 'handleWechatAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () { pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmWechatPayPayment( pretixstripe.stripe.confirmWechatPayPayment(
payment_intent_client_secret, payment_intent_client_secret,
{ {
payment_method_options: { payment_method_options: {
wechat_pay: { wechat_pay: {
client: 'web', client: 'web',
}, },
}, },
} }
).then(function (result) { ).then(function (result) {
if (result.error) { if (result.error) {
waitingDialog.hide() waitingDialog.hide();
$('.stripe-errors').stop().hide().removeClass('sr-only') $(".stripe-errors").stop().hide().removeClass("sr-only");
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>') $(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$('.stripe-errors').slideDown() $(".stripe-errors").slideDown();
} else { } else {
waitingDialog.show(gettext('Confirming your payment …')) waitingDialog.show(gettext("Confirming your payment …"));
location.reload() location.reload();
} }
}) });
}) });
}, },
handleCardAction: function (payment_intent_client_secret) { 'handleCardAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () { pretixstripe.withStripe(function () {
pretixstripe.stripe.handleCardAction( pretixstripe.stripe.handleCardAction(
payment_intent_client_secret payment_intent_client_secret
).then(function (result) { ).then(function (result) {
if (result.error) { if (result.error) {
waitingDialog.hide() waitingDialog.hide();
$('.stripe-errors').stop().hide().removeClass('sr-only') $(".stripe-errors").stop().hide().removeClass("sr-only");
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>') $(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$('.stripe-errors').slideDown() $(".stripe-errors").slideDown();
} else { } else {
waitingDialog.show(gettext('Confirming your payment …')) waitingDialog.show(gettext("Confirming your payment …"));
location.reload() location.reload();
} }
}) });
}) });
}, },
handlePaymentRedirectAction: function (payment_intent_next_action_redirect_url) { 'handlePaymentRedirectAction': function (payment_intent_next_action_redirect_url) {
waitingDialog.show(gettext('Contacting your bank …')) waitingDialog.show(gettext("Contacting your bank …"));
let payment_intent_redirect_action_handling = $.trim($('#stripe_payment_intent_redirect_action_handling').html()) let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
if (payment_intent_redirect_action_handling === 'iframe') { if (payment_intent_redirect_action_handling === 'iframe') {
let iframe = document.createElement('iframe') let iframe = document.createElement('iframe');
iframe.src = payment_intent_next_action_redirect_url iframe.src = payment_intent_next_action_redirect_url;
iframe.className = 'embed-responsive-item' iframe.className = 'embed-responsive-item';
$('#scacontainer').append(iframe) $('#scacontainer').append(iframe);
$('#scacontainer iframe').on('load', function () { $('#scacontainer iframe').on("load", function () {
waitingDialog.hide() waitingDialog.hide();
}) });
} else if (payment_intent_redirect_action_handling === 'redirect') { } else if (payment_intent_redirect_action_handling === 'redirect') {
window.location.href = payment_intent_next_action_redirect_url window.location.href = payment_intent_next_action_redirect_url;
} }
} }
} };
$(function () { $(function () {
if ($('#stripe_payment_intent_SCA_status').length) { if ($("#stripe_payment_intent_SCA_status").length) {
let payment_intent_redirect_action_handling = $.trim($('#stripe_payment_intent_redirect_action_handling').html()) let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
let order_status = $.trim($('#order_status').html()) let order_status = $.trim($("#order_status").html());
let order_url = $.trim($('#order_url').html()) let order_url = $.trim($("#order_url").html())
if (payment_intent_redirect_action_handling === 'iframe') { if (payment_intent_redirect_action_handling === 'iframe') {
window.parent.postMessage('3DS-authentication-complete.' + order_status, '*') window.parent.postMessage('3DS-authentication-complete.' + order_status, '*');
return return;
} else if (payment_intent_redirect_action_handling === 'redirect') { } else if (payment_intent_redirect_action_handling === 'redirect') {
waitingDialog.show(gettext('Confirming your payment …')) waitingDialog.show(gettext("Confirming your payment …"));
if (order_status === 'p') { if (order_status === 'p') {
window.location.href = order_url + '?paid=yes' window.location.href = order_url + '?paid=yes';
} else { } else {
window.location.href = order_url window.location.href = order_url;
} }
} }
} else if ($('#stripe_payment_intent_next_action_redirect_url').length) { } else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
let payment_intent_next_action_redirect_url = JSON.parse($('#stripe_payment_intent_next_action_redirect_url').html()) let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url) pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'promptpay_display_qr_code') { } else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
waitingDialog.hide() waitingDialog.hide();
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'wechat_pay_display_qr_code') { } else if ($.trim($("#stripe_payment_intent_action_type").html()) === "wechat_pay_display_qr_code") {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html()) let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleWechatAction(payment_intent_client_secret) pretixstripe.handleWechatAction(payment_intent_client_secret);
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'alipay_handle_redirect') { } else if ($.trim($("#stripe_payment_intent_action_type").html()) === "alipay_handle_redirect") {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html()) let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleAlipayAction(payment_intent_client_secret) pretixstripe.handleAlipayAction(payment_intent_client_secret);
} else if ($('#stripe_payment_intent_client_secret').length) { } else if ($("#stripe_payment_intent_client_secret").length) {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html()) let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleCardAction(payment_intent_client_secret) pretixstripe.handleCardAction(payment_intent_client_secret);
} }
$(window).on('message onmessage', function (e) { $(window).on("message onmessage", function (e) {
if (typeof e.originalEvent.data === 'string' && e.originalEvent.data.startsWith('3DS-authentication-complete.')) { if (typeof e.originalEvent.data === "string" && e.originalEvent.data.startsWith('3DS-authentication-complete.')) {
waitingDialog.show(gettext('Confirming your payment …')) waitingDialog.show(gettext("Confirming your payment …"));
$('#scacontainer').hide() $('#scacontainer').hide();
$('#continuebutton').removeClass('hidden') $('#continuebutton').removeClass('hidden');
if (e.originalEvent.data.split('.')[1] == 'p') { if (e.originalEvent.data.split('.')[1] == 'p') {
window.location.href = $('#continuebutton').attr('href') + '?paid=yes' window.location.href = $('#continuebutton').attr('href') + '?paid=yes';
} else { } else {
window.location.href = $('#continuebutton').attr('href') window.location.href = $('#continuebutton').attr('href');
} }
} }
}) });
if (!$('.stripe-container').length) if (!$(".stripe-container").length)
return return;
if ( if (
$('input[name=payment][value=stripe]').is(':checked') $("input[name=payment][value=stripe]").is(':checked')
|| $('input[name=payment][value=stripe_sepa_debit]').is(':checked') || $("input[name=payment][value=stripe_sepa_debit]").is(':checked')
|| $('input[name=payment][value=stripe_affirm]').is(':checked') || $("input[name=payment][value=stripe_affirm]").is(':checked')
|| $('input[name=payment][value=stripe_klarna]').is(':checked') || $("input[name=payment][value=stripe_klarna]").is(':checked')
|| $('.payment-redo-form').length) { || $(".payment-redo-form").length) {
pretixstripe.load() pretixstripe.load();
} else { } else {
$('input[name=payment]').change(function () { $("input[name=payment]").change(function () {
if (['stripe', 'stripe_sepa_debit', 'stripe_affirm', 'stripe_klarna'].indexOf($(this).val()) > -1) { if (['stripe', 'stripe_sepa_debit', 'stripe_affirm', 'stripe_klarna'].indexOf($(this).val()) > -1) {
pretixstripe.load() pretixstripe.load();
} }
}) })
} }
$('#stripe_other_card').click( $("#stripe_other_card").click(
function (e) { function (e) {
$('#stripe_card_payment_method_id').val('') $("#stripe_card_payment_method_id").val("");
$('#stripe-current-card').slideUp() $("#stripe-current-card").slideUp();
$('#stripe-card-elements').slideDown() $("#stripe-card-elements").slideDown();
e.preventDefault() e.preventDefault();
return false return false;
} }
) );
if ($('#stripe-current-card').length) { if ($("#stripe-current-card").length) {
$('#stripe-card-elements').hide() $("#stripe-card-elements").hide();
} }
$('#stripe_other_account').click( $("#stripe_other_account").click(
function (e) { function (e) {
$('#stripe_sepa_debit_payment_method_id').val('') $("#stripe_sepa_debit_payment_method_id").val("");
$('#stripe-current-account').slideUp() $("#stripe-current-account").slideUp();
// We're using a css-selector here instead of the id-selector, // We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields // as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').slideDown() $('.stripe-sepa_debit-form').slideDown();
e.preventDefault() e.preventDefault();
return false return false;
} }
) );
if ($('#stripe-current-account').length) { if ($("#stripe-current-account").length) {
// We're using a css-selector here instead of the id-selector, // We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields // as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').hide() $('.stripe-sepa_debit-form').hide();
} }
$('.stripe-container').closest('form').submit( $('.stripe-container').closest("form").submit(
function () { function () {
if ($('input[name=card_new]').length && !$('input[name=card_new]').prop('checked')) { if ($("input[name=card_new]").length && !$("input[name=card_new]").prop('checked')) {
return null return null;
} }
if (($('input[name=payment][value=stripe]').prop('checked') || $('input[name=payment][type=radio]').length === 0) if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment][type=radio]").length === 0)
&& $('#stripe_card_payment_method_id').val() == '') { && $("#stripe_card_payment_method_id").val() == "") {
pretixstripe.pm_request('card', pretixstripe.card) pretixstripe.pm_request('card', pretixstripe.card);
return false return false;
} }
if (($('input[name=payment][value=stripe_sepa_debit]').prop('checked')) && $('#stripe_sepa_debit_payment_method_id').val() == '') { if (($("input[name=payment][value=stripe_sepa_debit]").prop('checked')) && $("#stripe_sepa_debit_payment_method_id").val() == "") {
pretixstripe.pm_request('sepa_debit', pretixstripe.sepa, { pretixstripe.pm_request('sepa_debit', pretixstripe.sepa, {
billing_details: { billing_details: {
name: $('#id_payment_stripe_sepa_debit-accountname').val(), name: $("#id_payment_stripe_sepa_debit-accountname").val(),
email: $('#stripe_sepa_debit_email').val(), email: $("#stripe_sepa_debit_email").val(),
address: { address: {
line1: $('#id_payment_stripe_sepa_debit-line1').val(), line1: $("#id_payment_stripe_sepa_debit-line1").val(),
postal_code: $('#id_payment_stripe_sepa_debit-postal_code').val(), postal_code: $("#id_payment_stripe_sepa_debit-postal_code").val(),
city: $('#id_payment_stripe_sepa_debit-city').val(), city: $("#id_payment_stripe_sepa_debit-city").val(),
country: $('#id_payment_stripe_sepa_debit-country').val(), country: $("#id_payment_stripe_sepa_debit-country").val(),
} }
} }
}) });
return false return false;
} }
} }
) );
}) });
+3 -58
View File
@@ -35,7 +35,6 @@ import copy
import inspect import inspect
import uuid import uuid
from collections import defaultdict from collections import defaultdict
from datetime import time
from decimal import Decimal from decimal import Decimal
from django import forms from django import forms
@@ -53,7 +52,6 @@ from django.shortcuts import redirect
from django.utils import translation from django.utils import translation
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.html import conditional_escape from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import ( from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy, get_language, gettext_lazy as _, pgettext_lazy,
) )
@@ -73,7 +71,6 @@ from pretix.base.services.cart import (
from pretix.base.services.cross_selling import CrossSellingService from pretix.base.services.cross_selling import CrossSellingService
from pretix.base.services.memberships import validate_memberships_in_order from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order from pretix.base.services.orders import perform_order
from pretix.base.services.payment import compute_payment_deadline
from pretix.base.services.pricing import get_price from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import EventTask from pretix.base.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -843,8 +840,6 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
f.fields[fname].disabled = val['disabled'] f.fields[fname].disabled = val['disabled']
if 'validators' in val and fname in f.fields: if 'validators' in val and fname in f.fields:
f.fields[fname].validators += val['validators'] f.fields[fname].validators += val['validators']
if 'label' in val and fname in f.fields:
f.fields[fname].label = val['label']
return f return f
@@ -873,28 +868,6 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
'attendee_name_parts': d 'attendee_name_parts': d
}) })
wd = self.cart_session.get('widget_data', {})
if wd.get('attendee-fix', '') == 'true':
for k, v in wd.items():
if v and k.startswith('attendee-name'):
o.append({
'attendee_name_parts': {
'disabled': True,
}
})
elif v and k.startswith('email'):
o.append({
'attendee_email': {
'disabled': True,
}
})
elif v and k.startswith('question-'):
o.append({
k[9:].upper(): {
'disabled': True,
}
})
return o return o
@cached_property @cached_property
@@ -971,8 +944,6 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
f.fields[fname].disabled = val['disabled'] f.fields[fname].disabled = val['disabled']
if 'validators' in val and fname in f.fields: if 'validators' in val and fname in f.fields:
f.fields[fname].validators += val['validators'] f.fields[fname].validators += val['validators']
if 'label' in val and fname in f.fields:
f.fields[fname].label = val['label']
return f return f
@@ -1369,11 +1340,6 @@ class PaymentStep(CartMixin, TemplateFlowStep):
self.request = request self.request = request
self.request.pci_dss_payment_page = True self.request.pci_dss_payment_page = True
if "postpone" in request.POST and self._allow_postpone:
self.cart_session['payments_postpone'] = True
self.cart_session['payments'] = []
return redirect_to_url(self.get_next_url(request))
if "remove_payment" in request.POST: if "remove_payment" in request.POST:
self._remove_payment(request.POST["remove_payment"]) self._remove_payment(request.POST["remove_payment"])
return redirect_to_url(self.get_step_url(request)) return redirect_to_url(self.get_step_url(request))
@@ -1462,41 +1428,20 @@ class PaymentStep(CartMixin, TemplateFlowStep):
ctx['providers'] = self.provider_forms ctx['providers'] = self.provider_forms
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms) ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
if 'payment' in self.request.POST: if len(self.provider_forms) == 1:
ctx['selected'] = self.request.POST['payment']
elif self.cart_session.get('payments_postpone') and self._allow_postpone:
ctx['selected'] = ''
elif len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier ctx['selected'] = self.provider_forms[0]['provider'].identifier
elif 'payment' in self.request.POST:
ctx['selected'] = self.request.POST['payment']
elif self.single_use_payment: elif self.single_use_payment:
ctx['selected'] = self.single_use_payment['provider'] ctx['selected'] = self.single_use_payment['provider']
else: else:
ctx['selected'] = '' ctx['selected'] = ''
ctx['allow_postpone'] = self._allow_postpone
if self._allow_postpone:
now_dt = now()
ctx['payment_deadline'] = compute_payment_deadline(
event=self.request.event,
sales_channel=self.request.sales_channel,
subevents={p.subevent for p in ctx['cart']['raw']},
now_dt=now_dt,
)
if ctx['payment_deadline'].time() != time(hour=23, minute=59, second=59):
ctx['payment_deadline_minutes'] = int((ctx['payment_deadline'] - now_dt).total_seconds() // 60)
return ctx return ctx
@cached_property
def _allow_postpone(self):
return self.request.sales_channel.identifier in self.request.event.settings.payment_choice_postpone_allowed_channels
def _is_allowed(self, prov, request): def _is_allowed(self, prov, request):
return prov.is_allowed(request, total=self._total_order_value) return prov.is_allowed(request, total=self._total_order_value)
def is_completed(self, request, warn=False): def is_completed(self, request, warn=False):
if self.cart_session.get('payments_postpone') and self._allow_postpone:
return True
if not self.cart_session.get('payments'): if not self.cart_session.get('payments'):
if warn: if warn:
messages.error(request, _('Please select a payment method to proceed.')) messages.error(request, _('Please select a payment method to proceed.'))
+1 -1
View File
@@ -334,7 +334,7 @@ class ResetPasswordForm(forms.Form):
def clean_email(self): def clean_email(self):
if 'email' not in self.cleaned_data: if 'email' not in self.cleaned_data:
return return
if rate_limit("customer_pwreset_check", include_ip_from_request=self.request, max_num=10, expire_time=600): if rate_limit("customer_pwreset_check", max_num=10, expire_time=600):
raise forms.ValidationError( raise forms.ValidationError(
self.error_messages['rate_limit'], self.error_messages['rate_limit'],
code='rate_limit', code='rate_limit',
+2 -3
View File
@@ -247,15 +247,14 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
continue continue
if item.hidden_if_item_available: if item.hidden_if_item_available:
time_available = item.hidden_if_item_available.is_available()
if item.hidden_if_item_available.has_variations: if item.hidden_if_item_available.has_variations:
item._dependency_available = any( item._dependency_available = any(
var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK var.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)[0] == Quota.AVAILABILITY_OK
# is_available on variant is evaluated called by available_variations
for var in item.hidden_if_item_available.available_variations for var in item.hidden_if_item_available.available_variations
) and time_available )
else: else:
q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True) q = item.hidden_if_item_available.check_quotas(subevent=subevent, _cache=quota_cache, include_bundled=True)
time_available = item.hidden_if_item_available.is_available()
item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available item._dependency_available = (q[0] == Quota.AVAILABILITY_OK) and time_available
if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN: if item._dependency_available and item.hidden_if_item_available_mode == Item.UNAVAIL_MODE_HIDDEN:
item._remove = True item._remove = True
+4 -4
View File
@@ -233,7 +233,7 @@ Arguments: ``request``, ``order``
This signal allows you to override fields of the contact form that is presented during checkout This signal allows you to override fields of the contact form that is presented during checkout
and by default only asks for the email address. It is also being used for the invoice address and by default only asks for the email address. It is also being used for the invoice address
form. You are supposed to return a dictionary of dictionaries with globally unique keys. The form. You are supposed to return a dictionary of dictionaries with globally unique keys. The
value-dictionary should contain one or more of the following keys: ``label``, ``initial``, ``disabled``, value-dictionary should contain one or more of the following keys: ``initial``, ``disabled``,
``validators``. The key of the dictionary should be the name of the form field. ``validators``. The key of the dictionary should be the name of the form field.
As with all event plugin signals, the ``sender`` keyword argument will contain the event. A ``request`` As with all event plugin signals, the ``sender`` keyword argument will contain the event. A ``request``
@@ -264,9 +264,9 @@ Arguments: ``position``, ``request``
This signal allows you to override fields of the questions form that is presented during checkout This signal allows you to override fields of the questions form that is presented during checkout
and by default only asks for the questions configured in the backend. You are supposed to return a and by default only asks for the questions configured in the backend. You are supposed to return a
dictionary of dictionaries with globally unique keys. The value-dictionary should contain one or dictionary of dictionaries with globally unique keys. The value-dictionary should contain one or
more of the following keys: ``label``, ``initial``, ``disabled``, ``validators``. The key of the more of the following keys: ``initial``, ``disabled``, ``validators``. The key of the dictionary
dictionary should be the form field name for system fields (e.g. ``company``), or the question's should be the form field name for system fields (e.g. ``company``), or the question's ``identifier``
``identifier`` for user-defined questions. for user-defined questions.
The ``position`` keyword argument will contain a ``CartPosition`` or ``OrderPosition`` object. The ``position`` keyword argument will contain a ``CartPosition`` or ``OrderPosition`` object.
@@ -128,35 +128,6 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% if allow_postpone %}
<div class="panel panel-default">
<div class="panel-body row">
<div class="col-md-9 col-xs-12">
{% trans "Not sure yet? You can complete your order first and then select a payment method later." %}
<br>
<span class="text-muted">
{% if current_payments %}
{% trans "To do so, please first remove the payment methods you already selected above." %}
{% elif payment_deadline_minutes %}
{% blocktrans trimmed with minutes=payment_deadline_minutes %}
Your payment needs to be completed within {{ minutes }} minutes.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with deadline=payment_deadline|date:"SHORT_DATE_FORMAT" %}
Your payment needs to be completed by {{ deadline }}.
{% endblocktrans %}
{% endif %}
</span>
</div>
<div class="col-md-3 col-xs-12 text-right flip">
<button name="postpone" value="on" class="btn btn-primary"
{% if current_payments %}disabled{% endif %}>
{% trans "Proceed without selection" %}
</button>
</div>
</div>
</div>
{% endif %}
<div class="row checkout-button-row"> <div class="row checkout-button-row">
<div class="col-md-4 col-sm-6"> <div class="col-md-4 col-sm-6">
<a class="btn btn-block btn-default btn-lg" <a class="btn btn-block btn-default btn-lg"
@@ -23,11 +23,7 @@
{% endblocktrans %} :: {% endblocktrans %} ::
{% endif %} {% endif %}
{% elif subevent %} {% elif subevent %}
{% if subevent.name|upper != event.name|upper %} {{ subevent.get_date_range_display }} ::
{# The |upper is a trick to force LazyI18nString→str conversion before comparison #}
{{ subevent.name }} ::
{% endif %}
{{ subevent.get_date_range_display_with_times }} ::
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -139,6 +135,9 @@
{% if event_logo and event_logo_show_title %} {% if event_logo and event_logo_show_title %}
<h2 class="content-header"> <h2 class="content-header">
{{ event.name }} {{ event.name }}
{% if request.event.settings.show_dates_on_frontpage %}
<small>{{ event.get_date_range_display_as_html }}</small>
{% endif %}
</h2> </h2>
{% endif %} {% endif %}
{% if frontpage_text %} {% if frontpage_text %}
@@ -3,7 +3,7 @@
{% load escapejson %} {% load escapejson %}
{% if payment_qr_codes %} {% if payment_qr_codes %}
<div class="tabcontainer col-md-6 col-sm-6 col-xs-12 text-center js-only blank-after"> <div class="tabcontainer col-md-6 col-sm-6 hidden-xs text-center js-only blank-after">
<div id="banktransfer_qrcodes_tabs_content" class="tabpanels blank-after"> <div id="banktransfer_qrcodes_tabs_content" class="tabpanels blank-after">
{% for code_info in payment_qr_codes %} {% for code_info in payment_qr_codes %}
<div id="banktransfer_qrcodes_{{ code_info.id }}" <div id="banktransfer_qrcodes_{{ code_info.id }}"

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