Compare commits

...

26 Commits

Author SHA1 Message Date
Mira Weller
43b5c68f03 Rename migration 2025-11-07 11:35:45 +01:00
luelista
1cb2d443f9 Validation of user email addresses (#5434)
* Validation of user email addresses
* Improve email and password change forms
2025-11-07 11:17:34 +01:00
Raphael Michel
a0dbf6c5db Force Django upgrade (CVE-2025-64459) 2025-11-06 15:04:03 +01:00
Phin Wolkwitz
fd9d03786b Add program times for items (Z#23178639)
* Add program times for items

* Fix frontend date validation

* Add ical data for program times [wip]

* Improve ical data for program times

* Remove duplicate code and add comments

* Adjust migration

* Remove program times form for event series

* Add pdf placeholder [wip]

* Improve explanation text with suggestion

Co-authored-by: Raphael Michel <michel@pretix.eu>

* Fix import sorting

* Improve ical generation

* Improve ical entry description

* Fix migration

* Add copyability for program times fot items and events

* Update migration

* Add API endpoints/functions, fix isort

* Improve variable name

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Remove todo comment

* Add documentation, Change endpoint name

* Change related name

* Remove unnecessary code block

* Add program times to item API

* Fix imports

* Add log text

* Use daterange helper

* Add and update API tests

* Add another API test

* Add program times to cloning tests

* Update query count because of program times query

* Invalidate cached tickets on program time changes

* Reduce invalidation calls

* Update migration after rebase

* Apply improvements to invalidation from review

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* remove unneccessary attr=item param

* remove unnecessary kwargs for formset_factory

* fix local var name being overwritten in for-loop

* fix empty formset being saved

* Use subevent if available

* make code less verbose

* remove double event-label in ical desc

* fix unnecessary var re-assign

* fix ev vs p.subevent

---------

Co-authored-by: Raphael Michel <michel@pretix.eu>
Co-authored-by: Richard Schreiber <schreiber@rami.io>
2025-11-06 12:24:47 +01:00
Richard Schreiber
7041d40972 Invoice-PDF: split line.description into multiple rows so table can pagebreak (#5545) 2025-11-06 09:44:23 +01:00
Richard Schreiber
0b46982e6d Allow blocked seats to be booked in backend (#5585) 2025-11-06 08:02:42 +01:00
Yasunobu YesNo Kawaguchi
29906c6288 Translations: Update Japanese
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
CVZ-es
3380bd3e82 Translations: Update Spanish
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
CVZ-es
6ae8e7cbb6 Translations: Update French
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
CVZ-es
23c2d9266e Translations: Update Spanish
Currently translated at 99.9% (6130 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
CVZ-es
ba155faaa3 Translations: Update French
Currently translated at 99.9% (6131 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
Núria Masclans
fd177fa89f Translations: Update Catalan
Currently translated at 78.7% (200 of 254 strings)

Translation: pretix/pretix (JavaScript parts)
Translate-URL: https://translate.pretix.eu/projects/pretix/pretix-js/ca/

powered by weblate
2025-11-04 12:04:23 +01:00
CVZ-es
0b051c1400 Translations: Update Spanish
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
CVZ-es
af8d0f0b65 Translations: Update French
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
Raphael Michel
1b7bba195c Translations: Update German (informal) (de_Informal)
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
Raphael Michel
f056f77dc0 Translations: Update German
Currently translated at 100.0% (6132 of 6132 strings)

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

powered by weblate
2025-11-04 12:04:23 +01:00
Richard Schreiber
ee4e7f618f Sort answers according to questions order
* Sort answers according to questions (Z#23212280)

* Undo ordering in Meta-class

* filter and order answers only on invoice
2025-11-04 11:33:16 +01:00
dependabot[bot]
cd450f1780 Bump @babel/core from 7.28.4 to 7.28.5 in /src/pretix/static/npm_dir (#5579)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.28.4 to 7.28.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 11:26:29 +01:00
dependabot[bot]
fc876978b2 Bump @rollup/plugin-babel in /src/pretix/static/npm_dir (#5581)
Bumps [@rollup/plugin-babel](https://github.com/rollup/plugins/tree/HEAD/packages/babel) from 6.0.4 to 6.1.0.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/babel/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/url-v6.1.0/packages/babel)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-babel"
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 11:26:22 +01:00
dependabot[bot]
d8efaa47f7 Update css-inline requirement from ==0.17.* to ==0.18.*
Updates the requirements on [css-inline](https://github.com/Stranger6667/css-inline) to permit the latest version.
- [Release notes](https://github.com/Stranger6667/css-inline/releases)
- [Changelog](https://github.com/Stranger6667/css-inline/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stranger6667/css-inline/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: css-inline
  dependency-version: 0.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-04 11:25:45 +01:00
dependabot[bot]
f0c3514588 Bump @babel/preset-env in /src/pretix/static/npm_dir (#5580)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.28.3 to 7.28.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 17:40:47 +01:00
dependabot[bot]
e1ad4d9dba Bump @rollup/plugin-node-resolve in /src/pretix/static/npm_dir (#5578)
Bumps [@rollup/plugin-node-resolve](https://github.com/rollup/plugins/tree/HEAD/packages/node-resolve) from 16.0.1 to 16.0.3.
- [Changelog](https://github.com/rollup/plugins/blob/master/packages/node-resolve/CHANGELOG.md)
- [Commits](https://github.com/rollup/plugins/commits/node-resolve-v16.0.3/packages/node-resolve)

---
updated-dependencies:
- dependency-name: "@rollup/plugin-node-resolve"
  dependency-version: 16.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 17:40:15 +01:00
Raphael Michel
3ab587883e Translations: Add words to spellcheck wordlist 2025-11-03 17:40:04 +01:00
Raphael Michel
b02e1a1515 Gift card payment: Clean-up some code (#5574)
* Remove apparently unused code

* Move templates that do not belong in pretixcontrol
2025-11-03 17:38:56 +01:00
Raphael Michel
41780add40 Gift cards: Remove nested form tags 2025-10-30 18:03:47 +01:00
Raphael Michel
b07a61e4f1 Remove visible rounding mode 2025-10-30 17:54:06 +01:00
61 changed files with 2212 additions and 868 deletions

View File

@@ -0,0 +1,222 @@
Item program times
==================
Resource description
--------------------
Program times for products (items) that can be set in addition to event times, e.g. to display seperate schedules within an event.
The program times resource contains the following public fields:
.. rst-class:: rest-resource-table
===================================== ========================== =======================================================
Field Type Description
===================================== ========================== =======================================================
id integer Internal ID of the program time
start datetime The start date time for this program time slot.
end datetime The end date time for this program time slot.
===================================== ========================== =======================================================
.. versionchanged:: TODO
The resource has been added.
Endpoints
---------
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
Returns a list of all program times for a given item.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/11/program_times/ 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": 3,
"next": null,
"previous": null,
"results": [
{
"id": 2,
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
},
{
"id": 3,
"start": "2025-08-12T22:00:00Z",
"end": "2025-08-13T22:00:00Z"
},
{
"id": 14,
"start": "2025-08-15T22:00:00Z",
"end": "2025-08-17T22:00:00Z"
}
]
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event/item does not exist **or** you have no permission to view this resource.
.. http:get:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
Returns information on one program time, identified by its ID.
**Example request**:
.. sourcecode:: http
GET /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/1/ 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
{
"id": 1,
"start": "2025-08-15T22:00:00Z",
"end": "2025-10-27T23:00:00Z"
}
:param organizer: The ``slug`` field of the organizer to fetch
:param event: The ``slug`` field of the event to fetch
:param item: The ``id`` field of the item to fetch
:param id: The ``id`` field of the program time to fetch
:statuscode 200: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource.
.. http:post:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/
Creates a new program time
**Example request**:
.. sourcecode:: http
POST /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
{
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 201 Created
Vary: Accept
Content-Type: application/json
{
"id": 17,
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z"
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for
:param event: The ``slug`` field of the event to create a program time for
:param item: The ``id`` field of the item to create a program time for
:statuscode 201: no error
:statuscode 400: The program time could not be created due to invalid submitted data.
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to create this resource.
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/items/(item)/program_times/(id)/
Update a program time. 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/events/sampleconf/items/1/program_times/1/ HTTP/1.1
Host: pretix.eu
Accept: application/json, text/javascript
Content-Type: application/json
Content-Length: 94
{
"start": "2025-08-14T10:00:00Z"
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: application/json
{
"id": 1,
"start": "2025-08-14T10:00:00Z",
"end": "2025-08-15T12:00:00Z"
}
:param organizer: The ``slug`` field of the organizer to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to modify
:param id: The ``id`` field of the program time to modify
:statuscode 200: no error
:statuscode 400: The program time could not be modified due to invalid submitted data
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to change this resource.
.. http:delete:: /api/v1/organizers/(organizer)/events/(event)/items/(id)/program_times/(id)/
Delete a program time.
**Example request**:
.. sourcecode:: http
DELETE /api/v1/organizers/bigevents/events/sampleconf/items/1/program_times/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 to modify
:param event: The ``slug`` field of the event to modify
:param id: The ``id`` field of the item to modify
:param id: The ``id`` field of the program time to delete
:statuscode 204: no error
:statuscode 401: Authentication failure
:statuscode 403: The requested organizer/event does not exist **or** you have no permission to delete this resource.

View File

@@ -139,6 +139,9 @@ has_variations boolean Shows whether
variations list of objects A list with one object for each variation of this item.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
program_times list of objects A list with one object for each program time of this item.
Can be empty. Only writable during creation,
use separate endpoint to modify this later.
├ id integer Internal ID of the variation
├ value multi-lingual string The "name" of the variation
├ default_price money (string) The price set directly for this variation or ``null``
@@ -225,6 +228,10 @@ meta_data object Values set fo
The ``hidden_if_item_available_mode`` attributes has been added.
.. versionchanged:: 2025.9
The ``program_times`` attribute has been added.
Notes
-----
@@ -232,9 +239,9 @@ Please note that an item either always has variations or never has. Once created
change to an item without and vice versa. To create an item with variations ensure that you POST an item with at least
one variation.
Also note that ``variations``, ``bundles``, and ``addons`` are only supported on ``POST``. To update/delete variations,
bundles, and add-ons please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles`` and/or ``addons``.
Also note that ``variations``, ``bundles``, ``addons`` and ``program_times`` are only supported on ``POST``. To update/delete variations,
bundles, add-ons and program times please use the dedicated nested endpoints. By design this endpoint does not support ``PATCH`` and ``PUT``
with nested ``variations``, ``bundles``, ``addons`` and/or ``program_times``.
Endpoints
---------
@@ -373,7 +380,8 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": []
}
]
}
@@ -525,7 +533,8 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": []
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -653,7 +662,13 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": [
{
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
}
]
}
**Example response**:
@@ -773,7 +788,13 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": [
{
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z"
}
]
}
:param organizer: The ``slug`` field of the organizer of the event to create an item for
@@ -789,8 +810,9 @@ Endpoints
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 ``has_variations``, ``variations`` and the ``addon`` field. If
you need to update/delete variations or add-ons please use the nested dedicated endpoints.
You can change all fields of the resource except the ``has_variations``, ``variations``, ``addon`` and the
``program_times`` field. If you need to update/delete variations, add-ons or program times, please use the nested
dedicated endpoints.
**Example request**:
@@ -924,7 +946,8 @@ Endpoints
}
],
"addons": [],
"bundles": []
"bundles": [],
"program_times": []
}
:param organizer: The ``slug`` field of the organizer to modify

View File

@@ -33,9 +33,9 @@ dependencies = [
"celery==5.5.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"css-inline==0.17.*",
"css-inline==0.18.*",
"defusedcsv>=1.1.0",
"Django[argon2]==4.2.*,>=4.2.24",
"Django[argon2]==4.2.*,>=4.2.26",
"django-bootstrap3==25.2",
"django-compressor==4.5.1",
"django-countries==7.6.*",

View File

@@ -47,8 +47,9 @@ from pretix.api.serializers.event import MetaDataField
from pretix.api.serializers.fields import UploadedFileField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.base.models import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemVariation,
ItemVariationMetaValue, Question, QuestionOption, Quota, SalesChannel,
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue, ItemProgramTime,
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
SalesChannel,
)
@@ -187,6 +188,12 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
'position', 'price_included', 'multi_allowed')
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('start', 'end')
class ItemBundleSerializer(serializers.ModelSerializer):
class Meta:
model = ItemBundle
@@ -212,6 +219,31 @@ class ItemBundleSerializer(serializers.ModelSerializer):
return data
class ItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('id', 'start', 'end')
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)
start = full_data.get('start')
if not start:
raise ValidationError(_("The program start must not be empty."))
end = full_data.get('end')
if not end:
raise ValidationError(_("The program end must not be empty."))
if start > end:
raise ValidationError(_("The program end must not be before the program start."))
return data
class ItemAddOnSerializer(serializers.ModelSerializer):
class Meta:
model = ItemAddOn
@@ -250,6 +282,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
addons = InlineItemAddOnSerializer(many=True, required=False)
bundles = InlineItemBundleSerializer(many=True, required=False)
variations = InlineItemVariationSerializer(many=True, required=False)
program_times = InlineItemProgramTimeSerializer(many=True, required=False)
tax_rate = ItemTaxRateField(source='*', read_only=True)
meta_data = MetaDataField(required=False, source='*')
picture = UploadedFileField(required=False, allow_null=True, allowed_types=(
@@ -271,7 +304,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
'available_from', 'available_from_mode', 'available_until', 'available_until_mode',
'require_voucher', 'hide_without_voucher', 'allow_cancel', 'require_bundling',
'min_per_order', 'max_per_order', 'checkin_attention', 'checkin_text', 'has_variations', 'variations',
'addons', 'bundles', 'original_price', 'require_approval', 'generate_tickets',
'addons', 'bundles', 'program_times', 'original_price', 'require_approval', 'generate_tickets',
'show_quota_left', 'hidden_if_available', 'hidden_if_item_available', 'hidden_if_item_available_mode', 'allow_waitinglist',
'issue_giftcard', 'meta_data',
'require_membership', 'require_membership_types', 'require_membership_hidden', 'grant_membership_type',
@@ -294,9 +327,9 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
def validate(self, data):
data = super().validate(data)
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data):
raise ValidationError(_('Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use the '
'dedicated nested endpoint.'))
if self.instance and ('addons' in data or 'variations' in data or 'bundles' in data or 'program_times' in data):
raise ValidationError(_('Updating add-ons, bundles, program times or variations via PATCH/PUT is not '
'supported. Please use the dedicated nested endpoint.'))
Item.clean_per_order(data.get('min_per_order'), data.get('max_per_order'))
Item.clean_available(data.get('available_from'), data.get('available_until'))
@@ -347,6 +380,13 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
ItemAddOn.clean_max_min_count(addon_data.get('max_count', 0), addon_data.get('min_count', 0))
return value
def validate_program_times(self, value):
if not self.instance:
for program_time_data in value:
ItemProgramTime.clean_start_end(self, start=program_time_data.get('start', None),
end=program_time_data.get('end', None))
return value
@cached_property
def item_meta_properties(self):
return {
@@ -364,6 +404,7 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
variations_data = validated_data.pop('variations') if 'variations' in validated_data else {}
addons_data = validated_data.pop('addons') if 'addons' in validated_data else {}
bundles_data = validated_data.pop('bundles') if 'bundles' in validated_data else {}
program_times_data = validated_data.pop('program_times') if 'program_times' in validated_data else {}
meta_data = validated_data.pop('meta_data', None)
picture = validated_data.pop('picture', None)
require_membership_types = validated_data.pop('require_membership_types', [])
@@ -398,6 +439,8 @@ class ItemSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
ItemAddOn.objects.create(base_item=item, **addon_data)
for bundle_data in bundles_data:
ItemBundle.objects.create(base_item=item, **bundle_data)
for program_time_data in program_times_data:
ItemProgramTime.objects.create(item=item, **program_time_data)
# Meta data
if meta_data is not None:

View File

@@ -112,6 +112,7 @@ item_router = routers.DefaultRouter()
item_router.register(r'variations', item.ItemVariationViewSet)
item_router.register(r'addons', item.ItemAddOnViewSet)
item_router.register(r'bundles', item.ItemBundleViewSet)
item_router.register(r'program_times', item.ItemProgramTimeViewSet)
order_router = routers.DefaultRouter()
order_router.register(r'payments', order.PaymentViewSet)

View File

@@ -46,13 +46,13 @@ from rest_framework.response import Response
from pretix.api.pagination import TotalOrderingFilter
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
ItemSerializer, ItemVariationSerializer, QuestionOptionSerializer,
QuestionSerializer, QuotaSerializer,
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
)
from pretix.api.views import ConditionalListView
from pretix.base.models import (
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemVariation,
Question, QuestionOption, Quota,
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
ItemVariation, Question, QuestionOption, Quota,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.helpers.dicts import merge_dicts
@@ -279,6 +279,57 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
)
class ItemProgramTimeViewSet(viewsets.ModelViewSet):
serializer_class = ItemProgramTimeSerializer
queryset = ItemProgramTime.objects.none()
filter_backends = (DjangoFilterBackend, TotalOrderingFilter,)
ordering_fields = ('id',)
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
@cached_property
def item(self):
return get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
def get_queryset(self):
return self.item.program_times.all()
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.request.event
ctx['item'] = self.item
return ctx
def perform_create(self, serializer):
item = get_object_or_404(Item, pk=self.kwargs['item'], event=self.request.event)
serializer.save(item=item)
item.log_action(
'pretix.event.item.program_times.added',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_update(self, serializer):
serializer.save(event=self.request.event)
serializer.instance.item.log_action(
'pretix.event.item.program_times.changed',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(self.request.data, {'id': serializer.instance.pk})
)
def perform_destroy(self, instance):
super().perform_destroy(instance)
instance.item.log_action(
'pretix.event.item.program_times.removed',
user=self.request.user,
auth=self.request.auth,
data={'start': instance.start, 'end': instance.end}
)
class ItemAddOnViewSet(viewsets.ModelViewSet):
serializer_class = ItemAddOnSerializer
queryset = ItemAddOn.objects.none()

View File

@@ -214,21 +214,38 @@ class PasswordRecoverForm(forms.Form):
error_messages = {
'pw_mismatch': _("Please enter the same password twice"),
}
email = forms.EmailField(
max_length=255,
disabled=True,
label=_("Your email address"),
widget=forms.EmailInput(
attrs={'autocomplete': 'username'},
),
)
password = forms.CharField(
label=_('Password'),
widget=forms.PasswordInput,
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password',
}),
max_length=4096,
required=True
)
password_repeat = forms.CharField(
label=_('Repeat password'),
widget=forms.PasswordInput,
widget=forms.PasswordInput(attrs={
'autocomplete': 'new-password',
}),
max_length=4096,
)
def __init__(self, user_id=None, *args, **kwargs):
self.user_id = user_id
super().__init__(*args, **kwargs)
initial = kwargs.pop('initial', {})
try:
self.user = User.objects.get(id=user_id)
initial['email'] = self.user.email
except User.DoesNotExist:
self.user = None
super().__init__(*args, initial=initial, **kwargs)
def clean(self):
password1 = self.cleaned_data.get('password', '')
@@ -243,11 +260,7 @@ class PasswordRecoverForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
try:
user = User.objects.get(id=self.user_id)
except User.DoesNotExist:
user = None
if validate_password(password1, user=user) is not None:
if validate_password(password1, user=self.user) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1
@@ -307,3 +320,10 @@ class ReauthForm(forms.Form):
self.error_messages['inactive'],
code='inactive',
)
class ConfirmationCodeForm(forms.Form):
code = forms.IntegerField(
label=_('Confirmation code'),
widget=forms.NumberInput(attrs={'class': 'confirmation-code-input', 'inputmode': 'numeric', 'type': 'text'}),
)

View File

@@ -39,37 +39,16 @@ from django.contrib.auth.password_validation import (
password_validators_help_texts, validate_password,
)
from django.db.models import Q
from django.urls.base import reverse
from django.utils.translation import gettext_lazy as _
from pytz import common_timezones
from pretix.base.models import User
from pretix.control.forms import SingleLanguageWidget
from pretix.helpers.format import format_map
class UserSettingsForm(forms.ModelForm):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this email address. "
"Please choose a different one."),
'pw_current': _("Please enter your current password if you want to change your email address "
"or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
old_pw = forms.CharField(max_length=255,
required=False,
label=_("Your current password"),
widget=forms.PasswordInput())
new_pw = forms.CharField(max_length=255,
required=False,
label=_("New password"),
widget=forms.PasswordInput())
new_pw_repeat = forms.CharField(max_length=255,
required=False,
label=_("Repeat new password"),
widget=forms.PasswordInput())
timezone = forms.ChoiceField(
choices=((a, a) for a in common_timezones),
label=_("Default timezone"),
@@ -93,11 +72,60 @@ class UserSettingsForm(forms.ModelForm):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
self.fields['email'].required = True
if self.user.auth_backend != 'native':
del self.fields['old_pw']
del self.fields['new_pw']
del self.fields['new_pw_repeat']
self.fields['email'].disabled = True
self.fields['email'].disabled = True
self.fields['email'].help_text = format_map('<a href="{link}"><span class="fa fa-edit"></span> {text}</a>', {
'text': _("Change email address"),
'link': reverse('control:user.settings.email.change')
})
class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))
class UserPasswordChangeForm(forms.Form):
error_messages = {
'pw_current': _("Please enter your current password if you want to change your email address "
"or password."),
'pw_current_wrong': _("The current password you entered was not correct."),
'pw_mismatch': _("Please enter the same password twice"),
'rate_limit': _("For security reasons, please wait 5 minutes before you try again."),
'pw_equal': _("Please choose a password different to your current one.")
}
email = forms.EmailField(max_length=255,
disabled=True,
label=_("Your email address"),
widget=forms.EmailInput(
attrs={'autocomplete': 'username'},
))
old_pw = forms.CharField(max_length=255,
required=False,
label=_("Your current password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'current-password'},
))
new_pw = forms.CharField(max_length=255,
required=False,
label=_("New password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'new-password'},
))
new_pw_repeat = forms.CharField(max_length=255,
required=False,
label=_("Repeat new password"),
widget=forms.PasswordInput(
attrs={'autocomplete': 'new-password'},
))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
initial = kwargs.pop('initial', {})
initial['email'] = self.user.email
super().__init__(*args, initial=initial, **kwargs)
def clean_old_pw(self):
old_pw = self.cleaned_data.get('old_pw')
@@ -121,15 +149,6 @@ class UserSettingsForm(forms.ModelForm):
return old_pw
def clean_email(self):
email = self.cleaned_data['email']
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.instance.pk)).exists():
raise forms.ValidationError(
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
)
return email
def clean_new_pw(self):
password1 = self.cleaned_data.get('new_pw', '')
if password1 and validate_password(password1, user=self.user) is not None:
@@ -148,32 +167,24 @@ class UserSettingsForm(forms.ModelForm):
code='pw_mismatch'
)
def clean(self):
password1 = self.cleaned_data.get('new_pw')
email = self.cleaned_data.get('email')
old_pw = self.cleaned_data.get('old_pw')
if (password1 or email != self.user.email) and not old_pw:
class UserEmailChangeForm(forms.Form):
error_messages = {
'duplicate_identifier': _("There already is an account associated with this email address. "
"Please choose a different one."),
}
old_email = forms.EmailField(label=_('Old email address'), disabled=True)
new_email = forms.EmailField(label=_('New email address'))
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
def clean_new_email(self):
email = self.cleaned_data['new_email']
if User.objects.filter(Q(email__iexact=email) & ~Q(pk=self.user.pk)).exists():
raise forms.ValidationError(
self.error_messages['pw_current'],
code='pw_current'
self.error_messages['duplicate_identifier'],
code='duplicate_identifier',
)
if password1 and password1 == old_pw:
raise forms.ValidationError(
self.error_messages['pw_equal'],
code='pw_equal'
)
if password1:
self.instance.set_password(password1)
return self.cleaned_data
class User2FADeviceAddForm(forms.Form):
name = forms.CharField(label=_('Device name'), max_length=64)
devicetype = forms.ChoiceField(label=_('Device type'), widget=forms.RadioSelect, choices=(
('totp', _('Smartphone with the Authenticator application')),
('webauthn', _('WebAuthn-compatible hardware token (e.g. Yubikey)')),
))
return email

View File

@@ -23,6 +23,7 @@ import datetime
import logging
import math
import re
import textwrap
import unicodedata
from collections import defaultdict
from decimal import Decimal
@@ -752,11 +753,59 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return dt.astimezone(tz).date()
total = Decimal('0.00')
if has_taxes:
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
colwidths = [a * doc.width for a in (.65, .20, .15)]
for (description, tax_rate, tax_name, net_value, gross_value, subevent, period_start, period_end), lines in addon_aware_groupby(
all_lines,
key=_group_key,
is_addon=lambda l: l.description.startswith(" +"),
):
# split description into multiple Paragraphs so each fits in a table cell on a single page
# otherwise PDF-build fails
description_p_list = []
# normalize linebreaks to newlines instead of HTML so we can safely substring
description = description.replace('<br>', '<br />').replace('<br />\n', '\n').replace('<br />', '\n')
# start first line with different settings than the rest of the description
curr_description = description.split("\n", maxsplit=1)[0]
cellpadding = 6 # default cellpadding is only set on right side of column
max_width = colwidths[0] - cellpadding
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p_style
)
h = p.wrap(max_width, doc.height)[1]
if h <= max_height:
description_p_list.append(p)
if curr_description == description:
break
description = description[len(curr_description):].lstrip()
curr_description = description.split("\n", maxsplit=1)[0]
# use different settings for all except first line
max_width = sum(colwidths[0:3 if has_taxes else 2]) - cellpadding
max_height = self.stylesheet['Fineprint'].leading * 8
p_style = self.stylesheet['Fineprint']
continue
if not description_p_list:
# first "manual" line is larger than 5 "real" lines => only allow one line and set rest in Fineprint
max_height = self.stylesheet['Normal'].leading
if h > max_height * 1.1:
# quickly bring the text-length down to a managable length to then stepwise reduce
wrap_to = math.ceil(len(curr_description) * max_height * 1.1 / h)
else:
# trim to 95% length, but at most 10 chars to not have strangely short lines in the middle of a paragraph
wrap_to = max(len(curr_description) - 10, math.ceil(len(curr_description) * 0.95))
curr_description = textwrap.wrap(curr_description, wrap_to, replace_whitespace=False, drop_whitespace=False)[0]
# Try to be clever and figure out when organizers would want to show the period. This heuristic is
# not perfect and the only "fully correct" way would be to include the period on every line always,
# however this will cause confusion (a) due to useless repetition of the same date all over the invoice
@@ -810,7 +859,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice
request_show_service_date = period_line
elif period_line:
description += "\n" + period_line
description_p_list.append(FontFallbackParagraph(
period_line,
self.stylesheet['Fineprint']
))
lines = list(lines)
if has_taxes:
@@ -819,13 +871,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency),
)
description = description + "\n" + single_price_line
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
FontFallbackParagraph(
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
description_p_list.pop(0),
str(len(lines)),
localize(tax_rate) + " %",
FontFallbackParagraph(
@@ -837,23 +889,52 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.stylesheet['NormalRight']
),
))
for p in description_p_list:
tdata.append((p, "", "", "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(2, len(tdata) - 1),
))
else:
if len(lines) > 1:
single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency),
)
description = description + "\n" + single_price_line
description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append((
FontFallbackParagraph(
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
description_p_list.pop(0),
str(len(lines)),
FontFallbackParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight']
),
))
for p in description_p_list:
tdata.append((p, "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(1, len(tdata) - 1),
))
tstyledata += [
(
'BOTTOMPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 2),
0
),
(
'TOPPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 1),
0
),
]
taxvalue_map[tax_rate, tax_name] += (gross_value - net_value) * len(lines)
grossvalue_map[tax_rate, tax_name] += gross_value * len(lines)
total += gross_value * len(lines)
@@ -863,13 +944,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency)
])
colwidths = [a * doc.width for a in (.65, .20, .15)]
if not self.invoice.is_cancellation:
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING:

View File

@@ -0,0 +1,25 @@
# Generated by Django 4.2.19 on 2025-08-11 10:25
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0293_cartposition_price_includes_rounding_correction_and_more'),
]
operations = [
migrations.CreateModel(
name='ItemProgramTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('start', models.DateTimeField()),
('end', models.DateTimeField()),
('item',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='program_times',
to='pretixbase.item')),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 4.2.23 on 2025-09-04 16:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0294_item_program_time"),
]
operations = [
migrations.AddField(
model_name="user",
name="is_verified",
field=models.BooleanField(default=False),
),
]

View File

@@ -36,8 +36,9 @@ from .giftcards import GiftCard, GiftCardAcceptance, GiftCardTransaction
from .invoices import Invoice, InvoiceLine, invoice_filename
from .items import (
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
SubEventItem, SubEventItemVariation, itempicture_upload_to,
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
itempicture_upload_to,
)
from .log import LogEntry
from .media import ReusableMedium

View File

@@ -35,6 +35,7 @@
import binascii
import json
import operator
import secrets
from datetime import timedelta
from functools import reduce
@@ -44,6 +45,7 @@ from django.contrib.auth.models import (
)
from django.contrib.auth.tokens import default_token_generator
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import BadRequest, PermissionDenied
from django.db import IntegrityError, models, transaction
from django.db.models import Q
from django.utils.crypto import get_random_string, salted_hmac
@@ -239,9 +241,11 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
MAX_CONFIRMATION_CODE_ATTEMPTS = 10
email = models.EmailField(unique=True, db_index=True, null=True, blank=True,
verbose_name=_('Email'), max_length=190)
is_verified = models.BooleanField(default=False, verbose_name=_('Verified email address'))
fullname = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('Full name'))
is_active = models.BooleanField(default=True,
@@ -353,6 +357,77 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
except SendMailException:
pass # Already logged
def send_confirmation_code(self, session, reason, email=None, state=None):
"""
Sends a confirmation code via email to the user. The code is only valid for the action specified by `reason`.
The email is either sent to the email address currently on file for the user, or to the one given in the optional `email` parameter.
A `state` value can be provided which is bound to this confirmation code, and returned on successfully checking the code.
:param session: the user's request session
:param reason: the action which should be confirmed using this confirmation code (currently, only `email_change` is allowed)
:param email: optional, the email address to send the confirmation code to
:param state: optional
"""
from pretix.base.services.mail import mail
with language(self.locale):
if reason == 'email_change':
msg = str(_('to confirm changing your email address from {old_email}\nto {new_email}, use the following code:').format(
old_email=self.email, new_email=email,
))
elif reason == 'email_verify':
msg = str(_('to confirm that your email address {email} belongs to your pretix account, use the following code:').format(
email=self.email,
))
else:
raise Exception('Invalid confirmation code reason')
code = "%07d" % secrets.SystemRandom().randint(0, 9999999)
session['user_confirmation_code:' + reason] = {
'code': code,
'state': state,
'attempts': 0,
}
mail(
email or self.email,
_('pretix confirmation code'),
'pretixcontrol/email/confirmation_code.txt',
{
'user': self,
'reason': msg,
'code': code,
},
event=None,
user=self,
locale=self.locale
)
def check_confirmation_code(self, session, reason, code):
"""
Checks a confirmation code entered by the user against the valid code stored in the session.
If the code is correct, an optional state bound to the code is returned.
If the code is incorrect, PermissionDenied is raised. If the code could not be validated, either because no
code for the given reason is stored, or the number of input attempts is exceeded, BadRequest is raised.
:param session: the user's request session
:param reason: the action which should be confirmed using this confirmation code
:param code: the code entered by the user
:return: optional state bound to this code using the state parameter of send_confirmation_code, None otherwise
"""
stored = session.get('user_confirmation_code:' + reason)
if not stored:
raise BadRequest
if stored['attempts'] > User.MAX_CONFIRMATION_CODE_ATTEMPTS:
raise BadRequest
if int(stored['code']) == int(code):
del session['user_confirmation_code:' + reason]
return stored['state']
else:
stored['attempts'] += 1
session['user_confirmation_code:' + reason] = stored
raise PermissionDenied
def send_password_reset(self):
from pretix.base.services.mail import mail

View File

@@ -847,7 +847,7 @@ class Event(EventMixin, LoggedModel):
from ..signals import event_copy_data
from . import (
Discount, Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaValue,
ItemVariationMetaValue, Question, Quota,
ItemProgramTime, ItemVariationMetaValue, Question, Quota,
)
# Note: avoid self.set_active_plugins(), it causes trouble e.g. for the badges plugin.
@@ -990,6 +990,11 @@ class Event(EventMixin, LoggedModel):
ia.bundled_variation = variation_map[ia.bundled_variation.pk]
ia.save(force_insert=True)
for ipt in ItemProgramTime.objects.filter(item__event=other).prefetch_related('item'):
ipt.pk = None
ipt.item = item_map[ipt.item.pk]
ipt.save(force_insert=True)
quota_map = {}
for q in Quota.objects.filter(event=other, subevent__isnull=True).prefetch_related('items', 'variations'):
quota_map[q.pk] = q

View File

@@ -2294,3 +2294,27 @@ class ItemVariationMetaValue(LoggedModel):
class Meta:
unique_together = ('variation', 'property')
class ItemProgramTime(models.Model):
"""
This model can be used to add a program time to an item.
:param item: The item the program time applies to
:type item: Item
:param start: The date and time this program time starts
:type start: datetime
:param end: The date and time this program time ends
:type end: datetime
"""
item = models.ForeignKey('Item', related_name='program_times', on_delete=models.CASCADE)
start = models.DateTimeField(verbose_name=_("Start"))
end = models.DateTimeField(verbose_name=_("End"))
def clean(self):
self.clean_start_end(start=self.start, end=self.end)
super().clean()
def clean_start_end(self, start: datetime = None, end: datetime = None):
if start and end and start > end:
raise ValidationError(_("The program end must not be before the program start."))

View File

@@ -280,13 +280,13 @@ class Seat(models.Model):
def is_available(self, ignore_cart=None, ignore_orderpos=None, ignore_voucher_id=None,
sales_channel='web',
ignore_distancing=False, distance_ignore_cart_id=None):
ignore_distancing=False, distance_ignore_cart_id=None, always_allow_blocked=False):
from .orders import Order
from .organizer import SalesChannel
if isinstance(sales_channel, SalesChannel):
sales_channel = sales_channel.identifier
if self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
if not always_allow_blocked and self.blocked and sales_channel not in self.event.settings.seating_allow_blocked_seats_for_channel:
return False
opqs = self.orderposition_set.filter(
order__status__in=[Order.STATUS_PENDING, Order.STATUS_PAID],

View File

@@ -1377,7 +1377,7 @@ class GiftCardPayment(BasePaymentProvider):
execute_payment_needs_user = False
verbose_name = _("Gift card")
payment_form_class = GiftCardPaymentForm
payment_form_template_name = 'pretixcontrol/giftcards/checkout.html'
payment_form_template_name = 'pretixpresale/giftcard/checkout.html'
@cached_property
def customer_gift_cards(self):
@@ -1504,7 +1504,7 @@ class GiftCardPayment(BasePaymentProvider):
return super().order_change_allowed(order) and self.event.organizer.has_gift_cards
def checkout_confirm_render(self, request, order=None, info_data=None) -> str:
return get_template('pretixcontrol/giftcards/checkout_confirm.html').render({
return get_template('pretixpresale/giftcard/checkout_confirm.html').render({
'info_data': info_data,
})

View File

@@ -84,6 +84,7 @@ from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.phone_format import phone_format
from pretix.helpers.daterange import datetimerange
from pretix.helpers.reportlab import ThumbnailingImageReader, reshaper
from pretix.presale.style import get_fonts
@@ -490,6 +491,12 @@ DEFAULT_VARIABLES = OrderedDict((
"TIME_FORMAT"
) if op.valid_until else ""
}),
("program_times", {
"label": _("Program times: date and time"),
"editor_sample": _(
"2017-05-31 10:00 12:00\n2017-05-31 14:00 16:00\n2017-05-31 14:00 2017-06-01 14:00"),
"evaluate": lambda op, order, ev: get_program_times(op, ev)
}),
("medium_identifier", {
"label": _("Reusable Medium ID"),
"editor_sample": "ABC1234DEF4567",
@@ -734,6 +741,16 @@ def get_seat(op: OrderPosition):
return None
def get_program_times(op: OrderPosition, ev: Event):
return '\n'.join([
datetimerange(
pt.start.astimezone(ev.timezone),
pt.end.astimezone(ev.timezone),
as_html=False
) for pt in op.item.program_times.all()
])
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addons = [p for p in (

View File

@@ -258,9 +258,15 @@ def build_invoice(invoice: Invoice) -> Invoice:
if resp:
desc += "<br/>" + resp
for answ in p.answers.all():
if not answ.question.print_on_invoice:
continue
answers_qs = p.answers.filter(
question__print_on_invoice=True
).select_related(
'question'
).order_by(
'question__position',
'question__id'
)
for answ in answers_qs:
desc += "<br />{}{} {}".format(
answ.question.question,
"" if str(answ.question.question).endswith("?") else ":",

View File

@@ -1670,13 +1670,14 @@ class OrderChangeManager:
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True):
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
self.order = order
self.user = user
self.auth = auth
self.event = order.event
self.split_order = None
self.reissue_invoice = reissue_invoice
self.allow_blocked_seats = allow_blocked_seats
self._committed = False
self._totaldiff_guesstimate = 0
self._quotadiff = Counter()
@@ -2197,7 +2198,7 @@ class OrderChangeManager:
for seat, diff in self._seatdiff.items():
if diff <= 0:
continue
if not seat.is_available(sales_channel=self.order.sales_channel, ignore_distancing=True) or diff > 1:
if not seat.is_available(sales_channel=self.order.sales_channel, ignore_distancing=True, always_allow_blocked=self.allow_blocked_seats) or diff > 1:
raise OrderError(self.error_messages['seat_unavailable'].format(seat=seat.name))
if self.event.has_subevents:

View File

@@ -56,7 +56,8 @@ from i18nfield.forms import I18nFormField, I18nTextarea
from pretix.base.forms import I18nFormSet, I18nMarkdownTextarea, I18nModelForm
from pretix.base.forms.widgets import DatePickerWidget
from pretix.base.models import (
Item, ItemCategory, ItemVariation, Question, QuestionOption, Quota,
Item, ItemCategory, ItemProgramTime, ItemVariation, Question,
QuestionOption, Quota,
)
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
from pretix.base.signals import item_copy_data
@@ -572,6 +573,8 @@ class ItemCreateForm(I18nModelForm):
for b in self.cleaned_data['copy_from'].bundles.all():
instance.bundles.create(bundled_item=b.bundled_item, bundled_variation=b.bundled_variation,
count=b.count, designated_price=b.designated_price)
for pt in self.cleaned_data['copy_from'].program_times.all():
instance.program_times.create(start=pt.start, end=pt.end)
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
@@ -1321,3 +1324,49 @@ class ItemMetaValueForm(forms.ModelForm):
widgets = {
'value': forms.TextInput()
}
class ItemProgramTimeFormSet(I18nFormSet):
template = "pretixcontrol/item/include_program_times.html"
title = _('Program times')
def _construct_form(self, i, **kwargs):
kwargs['event'] = self.event
return super()._construct_form(i, **kwargs)
@property
def empty_form(self):
self.is_valid()
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
use_required_attribute=False,
locales=self.locales,
event=self.event
)
self.add_fields(form, None)
return form
class ItemProgramTimeForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['end'].widget.attrs['data-date-after'] = '#id_{prefix}-start_0'.format(prefix=self.prefix)
class Meta:
model = ItemProgramTime
localized_fields = '__all__'
fields = [
'start',
'end',
]
field_classes = {
'start': forms.SplitDateTimeField,
'end': forms.SplitDateTimeField,
}
widgets = {
'start': SplitDateTimePickerWidget(),
'end': SplitDateTimePickerWidget(),
}

View File

@@ -69,6 +69,7 @@ class UserEditForm(forms.ModelForm):
'email',
'require_2fa',
'is_active',
'is_verified',
'is_staff',
'needs_password_change',
'last_login'

View File

@@ -667,6 +667,14 @@ class UserSettingsChangedLogEntryType(LogEntryType):
return text
@log_entry_types.new_from_dict({
'pretix.user.email.changed': _('Your email address has been changed from {old_email} to {email}.'),
'pretix.user.email.confirmed': _('Your email address {email} has been confirmed.'),
})
class UserEmailChangedLogEntryType(LogEntryType):
pass
class UserImpersonatedLogEntryType(LogEntryType):
def display(self, logentry, data):
return self.plain.format(data['other_email'])
@@ -882,6 +890,9 @@ class EventPluginStateLogEntryType(EventLogEntryType):
'pretix.event.item.bundles.added': _('A bundled item has been added to this product.'),
'pretix.event.item.bundles.removed': _('A bundled item has been removed from this product.'),
'pretix.event.item.bundles.changed': _('A bundled item has been changed on this product.'),
'pretix.event.item.program_times.added': _('A program time has been added to this product.'),
'pretix.event.item.program_times.changed': _('A program time has been changed on this product.'),
'pretix.event.item.program_times.removed': _('A program time has been removed from this product.'),
})
class CoreItemLogEntryType(ItemLogEntryType):
pass

View File

@@ -72,7 +72,7 @@ class PermissionMiddleware:
)
EXCEPTIONS_FORCED_PW_CHANGE = (
"user.settings",
"user.settings.password.change",
"auth.logout"
)
@@ -139,7 +139,7 @@ class PermissionMiddleware:
return redirect_to_url(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
except SessionPasswordChangeRequired:
if url_name not in self.EXCEPTIONS_FORCED_PW_CHANGE:
return redirect_to_url(reverse('control:user.settings') + '?next=' + quote(request.get_full_path()))
return redirect_to_url(reverse('control:user.settings.password.change') + '?next=' + quote(request.get_full_path()))
except Session2FASetupRequired:
if url_name not in self.EXCEPTIONS_2FA:
return redirect_to_url(reverse('control:user.settings.2fa'))

View File

@@ -7,6 +7,7 @@
<h3>{% trans "Set new password" %}</h3>
{% csrf_token %}
{% bootstrap_form_errors form type='all' layout='inline' %}
{% bootstrap_field form.email %}
{% bootstrap_field form.password %}
{% bootstrap_field form.password_repeat %}
<div class="form-group buttons">

View File

@@ -0,0 +1,13 @@
{% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello,
{{ reason }}
{{ code }}
Please do never give this code to another person. Our support team will never ask for this code.
If this code was not requested by you, please contact us immediately.
Best regards,
Your pretix team
{% endblocktrans %}

View File

@@ -1,35 +0,0 @@
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% load rich_text %}
{{ request.event.settings.payment_giftcard_public_description|rich_text }}
{% if customer_gift_cards %}
<p><strong>
<span class="sr-only">{% trans "Information" %}</span>
{% trans "The following gift cards are available in your customer account:" %}
</strong></p>
<form method="post">
{% csrf_token %}
<ul class="list-group">
{% for c in customer_gift_cards %}
<li class="list-group-item row row-no-gutters">
<div class="col-sm-8 col-md-9" id="gc-code-{{ forloop.counter }}">
{{ c }}
</div>
<div class="col-sm-2 text-right" id="gc-value-{{ forloop.counter }}">
{{ c.value|money:c.currency }}
</div>
<div class="col-sm-2 col-md-1 text-right">
<button name="use_giftcard" class="btn btn-primary btn-xs use_giftcard" data-value="{{ c.secret }}"
title="{% trans "Use gift card" %}"
aria-describedby="gc-code-{{ forloop.counter }} gc-value-{{ forloop.counter }}">
{% trans "Apply" %}
</button>
</div>
</li>
{% endfor %}
</ul>
</form>
{% endif %}
{% bootstrap_form form layout='checkout' %}

View File

@@ -0,0 +1,70 @@
{% load i18n %}
{% load bootstrap3 %}
{% load formset_tags %}
<p>
{% blocktrans trimmed %}
With program times, you can set specific dates and times for this product.
This is useful if this product represents access to parts of your event that happen at different times than your event in general.
This will not affect access control, but will affect calendar invites and ticket output.
{% endblocktrans %}
</p>
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
{{ formset.management_form }}
{% bootstrap_formset_errors formset %}
<div data-formset-body>
{% for form in formset %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ form.id }}
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Program time" %}</h3>
</div>
<div class="col-sm-4 text-right flip">
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_form_errors form %}
{% bootstrap_field form.start layout="control" %}
{% bootstrap_field form.end layout="control" %}
</div>
</div>
{% endfor %}
</div>
<script type="form-template" data-formset-empty-form>
{% escapescript %}
<div class="panel panel-default" data-formset-form>
<div class="sr-only">
{{ formset.empty_form.id }}
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
</div>
<div class="panel-heading">
<div class="row">
<div class="col-sm-8">
<h3 class="panel-title">{% trans "Program time" %}</h3>
</div>
<div class="col-sm-4 text-right flip">
<button type="button" class="btn btn-xs btn-danger" data-formset-delete-button>
<i class="fa fa-trash"></i></button>
</div>
</div>
</div>
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.start layout="control" %}
{% bootstrap_field formset.empty_form.end layout="control" %}
</div>
</div>
{% endescapescript %}
</script>
<p>
<button type="button" class="btn btn-default" data-formset-add>
<i class="fa fa-plus"></i> {% trans "Add a program time" %}</button>
</p>
</div>

View File

@@ -769,10 +769,12 @@
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
<strong>{{ items.total|money:event.currency }}</strong>
<br/>
<small class="admin-only">
tax_rounding_mode = {{ order.tax_rounding_mode }}
</small>
{% if django_settings.DEBUG %}
<br/>
<small class="admin-only">
tax_rounding_mode = {{ order.tax_rounding_mode }}
</small>
{% endif %}
</div>
<div class="clearfix"></div>
</div>

View File

@@ -0,0 +1,29 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Change login email address" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Change login email address" %}
</h1>
{% csrf_token %}
{% bootstrap_form_errors form %}
<p class="text-muted">
{% trans "This changes the email address used to login to your account, as well as where we send email notifications." %}
</p>
{% bootstrap_field form.old_email %}
{% bootstrap_field form.new_email %}
<p>
{% trans "We will send a confirmation code to your new email address, which you need to enter in the next step to confirm the email address is correct." %}
</p>
<div class="form-group submit-group">
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Continue" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Change password" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Change password" %}
</h1>
<br>
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.email %}
{% bootstrap_field form.old_pw %}
{% bootstrap_field form.new_pw %}
{% bootstrap_field form.new_pw_repeat %}
<div class="form-group submit-group">
<a href="{% url "control:user.settings" %}" class="btn btn-default btn-cancel">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Change password" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Enter confirmation code" %}{% endblock %}
{% block content %}
<form action="" method="post" class="form centered-form">
<h1>
{% trans "Enter confirmation code" %}
</h1>
{% csrf_token %}
{% bootstrap_form_errors form type='all' layout='inline' %}
<p>{{ message }}</p>
{% bootstrap_field form.code %}
<div class="form-group submit-group">
<a href="{{ cancel_url }}" class="btn btn-default btn-cancel">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-primary btn-save btn-lg">
{% trans "Continue" %}
</button>
</div>
</form>
{% endblock %}

View File

@@ -3,8 +3,26 @@
{% load bootstrap3 %}
{% block title %}{% trans "Account settings" %}{% endblock %}
{% block content %}
{% if not user.is_verified %}
<div class="alert alert-info">
<p>
{% blocktrans trimmed %}
Your email address is not confirmed yet. To secure your account, please confirm your email address using
a confirmation code we will send to your email address.
{% endblocktrans %}
</p>
<p>
<form action="{% url "control:user.settings.email.send_verification_code" %}" method="post" class="form-horizontal">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
{% trans "Send confirmation email" %}
</button>
</form>
</p>
</div>
{% endif %}
<h1>{% trans "Account settings" %}</h1>
<form action="" method="post" class="form-horizontal">
<form action="" method="post" class="form-horizontal" data-testid="usersettingsform">
{% csrf_token %}
{% bootstrap_form_errors form %}
<fieldset>
@@ -13,7 +31,7 @@
{% bootstrap_field form.locale layout='horizontal' %}
{% bootstrap_field form.timezone layout='horizontal' %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Notifications" %}</label>
<label class="col-md-3 control-label">{% trans "Notifications" %}</label>
<div class="col-md-9 static-form-row">
{% if request.user.notifications_send and request.user.notification_settings.exists %}
<span class="label label-success">
@@ -41,8 +59,18 @@
{% bootstrap_field form.new_pw layout='horizontal' %}
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
{% endif %}
{% if user.auth_backend == 'native' %}
<div class="form-group">
<label class="col-md-3 control-label">{% trans "Password" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.password.change" %}">
<span class="fa fa-edit"></span> {% trans "Change password" %}
</a>
</div>
</div>
{% endif %}
<div class="form-group">
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Two-factor authentication" %}</label>
<label class="col-md-3 control-label">{% trans "Two-factor authentication" %}</label>
<div class="col-md-9 static-form-row">
{% if user.require_2fa %}
<span class="label label-success">{% trans "Enabled" %}</span> &nbsp;
@@ -58,7 +86,7 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="">{% trans "Authorized applications" %}</label>
<label class="col-md-3 control-label">{% trans "Authorized applications" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.oauth.list" %}">
<span class="fa fa-plug"></span>
@@ -67,7 +95,7 @@
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label" for="">{% trans "Account history" %}</label>
<label class="col-md-3 control-label">{% trans "Account history" %}</label>
<div class="col-md-9 static-form-row">
<a href="{% url "control:user.settings.history" %}">
<span class="fa fa-history"></span>

View File

@@ -56,6 +56,7 @@
{% if form.new_pw %}
{% bootstrap_field form.new_pw layout='control' %}
{% bootstrap_field form.new_pw_repeat layout='control' %}
{% bootstrap_field form.is_verified layout='control' %}
{% endif %}
{% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}

View File

@@ -110,6 +110,10 @@ urlpatterns = [
name='user.settings.2fa.confirm.webauthn'),
re_path(r'^settings/2fa/(?P<devicetype>[^/]+)/(?P<device>[0-9]+)/delete', user.User2FADeviceDeleteView.as_view(),
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/change$', user.UserEmailChangeView.as_view(), name='user.settings.email.change'),
re_path(r'^settings/email/verify', user.UserEmailVerifyView.as_view(), name='user.settings.email.send_verification_code'),
re_path(r'^settings/password/change$', user.UserPasswordChangeView.as_view(), name='user.settings.password.change'),
re_path(r'^organizers/$', organizer.OrganizerList.as_view(), name='organizers'),
re_path(r'^organizers/add$', organizer.OrganizerCreate.as_view(), name='organizers.add'),
re_path(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),

View File

@@ -60,13 +60,14 @@ from django.views.generic.detail import DetailView, SingleObjectMixin
from django_countries.fields import Country
from pretix.api.serializers.item import (
ItemAddOnSerializer, ItemBundleSerializer, ItemVariationSerializer,
ItemAddOnSerializer, ItemBundleSerializer, ItemProgramTimeSerializer,
ItemVariationSerializer,
)
from pretix.base.forms import I18nFormSet
from pretix.base.models import (
CartPosition, Item, ItemCategory, ItemVariation, Order, OrderPosition,
Question, QuestionAnswer, QuestionOption, Quota, SeatCategoryMapping,
Voucher,
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, Order,
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
SeatCategoryMapping, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
@@ -75,9 +76,9 @@ from pretix.base.services.tickets import invalidate_cache
from pretix.base.signals import quota_availability
from pretix.control.forms.item import (
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemUpdateForm,
ItemVariationForm, ItemVariationsFormSet, QuestionForm, QuestionOptionForm,
QuotaForm,
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
)
from pretix.control.permissions import (
EventPermissionRequiredMixin, event_permission_required,
@@ -1431,7 +1432,8 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
form.instance.position = i
setattr(form.instance, attr, self.get_object())
created = not form.instance.pk
form.save()
if form.has_changed():
form.save()
if form.has_changed() and any(a for a in form.changed_data if a != 'ORDER'):
change_data = {k: form.cleaned_data.get(k) for k in form.changed_data}
if key == 'variations':
@@ -1497,6 +1499,16 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
'bundles', 'bundles', 'base_item', order=False,
serializer=ItemBundleSerializer
)
elif k == 'program_times':
self.save_formset(
'program_times', 'program_times', order=False,
serializer=ItemProgramTimeSerializer
)
if not change_data:
for f in v.forms:
if (f in v.deleted_forms and f.instance.pk) or f.has_changed():
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
break
else:
v.save()
@@ -1559,9 +1571,20 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
queryset=ItemBundle.objects.filter(base_item=self.get_object()),
event=self.request.event, item=self.item, prefix="bundles"
)),
('program_times', inlineformset_factory(
Item, ItemProgramTime,
form=ItemProgramTimeForm, formset=ItemProgramTimeFormSet,
can_order=False, can_delete=True, extra=0
)(
self.request.POST if self.request.method == "POST" else None,
queryset=ItemProgramTime.objects.filter(item=self.get_object()),
event=self.request.event, prefix="program_times"
)),
])
if not self.object.has_variations:
del f['variations']
if self.item.event.has_subevents:
del f['program_times']
i = 0
for rec, resp in item_formsets.send(sender=self.request.event, item=self.item, request=self.request):

View File

@@ -2146,7 +2146,8 @@ class OrderChange(OrderView):
self.order,
user=self.request.user,
notify=notify,
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True
reissue_invoice=self.other_form.cleaned_data['reissue_invoice'] if self.other_form.is_valid() else True,
allow_blocked_seats=True,
)
form_valid = (self._process_add_fees(ocm) and
self._process_add_positions(ocm) and

View File

@@ -44,7 +44,9 @@ from pypdf.errors import PdfReadError
from reportlab.lib.units import mm
from pretix.base.i18n import language
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
from pretix.base.models import (
CachedFile, InvoiceAddress, ItemProgramTime, OrderPosition,
)
from pretix.base.pdf import get_images, get_variables
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.control.permissions import EventPermissionRequiredMixin
@@ -95,6 +97,9 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
description=_("Sample product description"))
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=Decimal('23.40'))
ItemProgramTime.objects.create(start=now(), end=now(), item=item)
ItemProgramTime.objects.create(start=now(), end=now(), item=item2)
from pretix.base.models import Order
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',

View File

@@ -44,11 +44,13 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import BadRequest, PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.html import format_html
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 _
@@ -60,8 +62,11 @@ from django_scopes import scopes_disabled
from webauthn.helpers import generate_challenge, generate_user_handle
from pretix.base.auth import get_auth_backends
from pretix.base.forms.auth import ReauthForm
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
from pretix.base.forms.auth import ConfirmationCodeForm, ReauthForm
from pretix.base.forms.user import (
User2FADeviceAddForm, UserEmailChangeForm, UserPasswordChangeForm,
UserSettingsForm,
)
from pretix.base.models import (
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
)
@@ -237,25 +242,7 @@ class UserSettings(UpdateView):
data = {}
for k in form.changed_data:
if k not in ('old_pw', 'new_pw_repeat'):
if 'new_pw' == k:
data['new_pw'] = True
else:
data[k] = form.cleaned_data[k]
msgs = []
if 'new_pw' in form.changed_data:
self.request.user.needs_password_change = False
msgs.append(_('Your password has been changed.'))
if 'email' in form.changed_data:
msgs.append(_('Your email address has been changed to {email}.').format(email=form.cleaned_data['email']))
if msgs:
self.request.user.send_security_notice(msgs, email=form.cleaned_data['email'])
if self._old_email != form.cleaned_data['email']:
self.request.user.send_security_notice(msgs, email=self._old_email)
data[k] = form.cleaned_data[k]
sup = super().form_valid(form)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
@@ -834,3 +821,159 @@ class EditStaffSession(StaffMemberRequiredMixin, UpdateView):
return get_object_or_404(StaffSession, pk=self.kwargs['id'])
else:
return get_object_or_404(StaffSession, pk=self.kwargs['id'], user=self.request.user)
class UserPasswordChangeView(FormView):
max_time = 300
form_class = UserPasswordChangeForm
template_name = 'pretixcontrol/user/change_password.html'
def get_form_kwargs(self):
if self.request.user.auth_backend != 'native':
raise PermissionDenied
return {
**super().get_form_kwargs(),
"user": self.request.user,
}
def form_valid(self, form):
with transaction.atomic():
self.request.user.set_password(form.cleaned_data['new_pw'])
self.request.user.needs_password_change = False
self.request.user.save()
msgs = []
msgs.append(_('Your password has been changed.'))
self.request.user.send_security_notice(msgs)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data={'new_pw': True})
update_session_auth_hash(self.request, self.request.user)
messages.success(self.request, _('Your changes have been saved.'))
return redirect(self.get_success_url())
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
def get_success_url(self):
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
return self.request.GET.get("next")
return reverse('control:user.settings')
class UserEmailChangeView(RecentAuthenticationRequiredMixin, FormView):
max_time = 300
form_class = UserEmailChangeForm
template_name = 'pretixcontrol/user/change_email.html'
def get_form_kwargs(self):
if self.request.user.auth_backend != 'native':
raise PermissionDenied
return {
**super().get_form_kwargs(),
"user": self.request.user,
}
def get_initial(self):
return {
"old_email": self.request.user.email
}
def form_valid(self, form):
self.request.user.send_confirmation_code(
session=self.request.session,
reason='email_change',
email=form.cleaned_data['new_email'],
state=form.cleaned_data['new_email'],
)
self.request.session['email_confirmation_destination'] = form.cleaned_data['new_email']
return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_change')
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class UserEmailVerifyView(View):
def post(self, request, *args, **kwargs):
if self.request.user.is_verified:
messages.success(self.request, _('Your email address was already verified.'))
return redirect(reverse('control:user.settings', kwargs={}))
self.request.user.send_confirmation_code(
session=self.request.session,
reason='email_verify',
email=self.request.user.email,
state=self.request.user.email,
)
self.request.session['email_confirmation_destination'] = self.request.user.email
return redirect(reverse('control:user.settings.email.confirm', kwargs={}) + '?reason=email_verify')
class UserEmailConfirmView(FormView):
form_class = ConfirmationCodeForm
template_name = 'pretixcontrol/user/confirmation_code_dialog.html'
def get_context_data(self, **kwargs):
return {
**super().get_context_data(**kwargs),
"cancel_url": reverse('control:user.settings', kwargs={}),
"message": format_html(
_("Please enter the confirmation code we sent to your email address <strong>{email}</strong>."),
email=self.request.session.get('email_confirmation_destination', ''),
),
}
@transaction.atomic()
def form_valid(self, form):
reason = self.request.GET['reason']
if reason not in ('email_change', 'email_verify'):
raise PermissionDenied
try:
new_email = self.request.user.check_confirmation_code(
session=self.request.session,
reason=reason,
code=form.cleaned_data['code'],
)
except PermissionDenied:
return self.form_invalid(form)
except BadRequest:
messages.error(self.request, _(
'We were unable to verify your confirmation code. Please try again.'
))
return redirect(reverse('control:user.settings', kwargs={}))
log_data = {
'email': new_email,
'email_verified': True,
}
if reason == 'email_change':
msgs = []
msgs.append(_('Your email address has been changed to {email}.').format(email=new_email))
log_data['old_email'] = old_email = self.request.user.email
self.request.user.send_security_notice(msgs, email=old_email)
self.request.user.send_security_notice(msgs, email=new_email)
log_action = 'pretix.user.email.changed'
else:
log_action = 'pretix.user.email.confirmed'
self.request.user.email = new_email
self.request.user.is_verified = True
self.request.user.save()
self.request.user.log_action(log_action, user=self.request.user, data=log_data)
update_session_auth_hash(self.request, self.request.user)
if reason == 'email_change':
messages.success(self.request, _('Your email address has been changed successfully.'))
else:
messages.success(self.request, _('Your email address has been confirmed successfully.'))
return redirect(reverse('control:user.settings', kwargs={}))
def form_invalid(self, form):
messages.error(self.request, _('The entered confirmation code is not correct. Please try again.'))
return super().form_invalid(form)

View File

@@ -8,10 +8,10 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-29 07:55+0000\n"
"PO-Revision-Date: 2025-10-28 17:00+0000\n"
"PO-Revision-Date: 2025-10-31 17:00+0000\n"
"Last-Translator: Núria Masclans <nuriamasclansserrat@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
"js/ca/>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/"
"pretix-js/ca/>\n"
"Language: ca\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -651,6 +651,8 @@ msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
"El color no té prou contrast amb el blanc i pot afectar a l'accessibilitat "
"del lloc web."
#: pretix/static/pretixcontrol/js/ui/main.js:418
#: pretix/static/pretixcontrol/js/ui/main.js:438
@@ -712,229 +714,226 @@ msgstr[0] "(una data més)"
msgstr[1] "({num} dates més)"
#: pretix/static/pretixpresale/js/ui/cart.js:47
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr "El contingut de la cistella ja no el teniu reservat."
msgstr ""
"Ja no tens reservat el contingut de la cistella. Encara pots completar la "
"comanda si els articles seleccionats continuen disponibles."
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"
msgstr "Cistella expirada"
msgstr "Cistella caducada"
#: pretix/static/pretixpresale/js/ui/cart.js:58
#: pretix/static/pretixpresale/js/ui/cart.js:84
msgid "Your cart is about to expire."
msgstr ""
msgstr "La teva cistella està a punt de caducar."
#: pretix/static/pretixpresale/js/ui/cart.js:62
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid "The items in your cart are reserved for you for one minute."
msgid_plural "The items in your cart are reserved for you for {num} minutes."
msgstr[0] "El contingut de la cistella ja no el teniu reservat."
msgstr[1] "El contingut de la cistella ja no el teniu reservat."
msgstr[0] "El contingut de la teva cistella està reservat durant un minut."
msgstr[1] "El contingut de la teva cistella està reservat durant {num} minuts."
#: pretix/static/pretixpresale/js/ui/cart.js:83
#, fuzzy
#| msgid "Cart expired"
msgid "Your cart has expired."
msgstr "Cistella expirada"
msgstr "La teva cistella ha caducat."
#: pretix/static/pretixpresale/js/ui/cart.js:86
#, fuzzy
#| msgid "The items in your cart are no longer reserved for you."
msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as they're available."
msgstr "El contingut de la cistella ja no el teniu reservat."
msgstr ""
"Ja no teniu reservat el contingut de la cistella. Encara podeu completar la "
"comanda si els articles continuen disponibles."
#: pretix/static/pretixpresale/js/ui/cart.js:87
msgid "Do you want to renew the reservation period?"
msgstr ""
msgstr "Vols ampliar el temps de reserva?"
#: pretix/static/pretixpresale/js/ui/cart.js:90
msgid "Renew reservation"
msgstr ""
msgstr "Renovar reserva"
#: pretix/static/pretixpresale/js/ui/main.js:194
msgid "The organizer keeps %(currency)s %(amount)s"
msgstr ""
msgstr "L'entitat organitzadora es queda %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:202
msgid "You get %(currency)s %(amount)s back"
msgstr ""
msgstr "Rebràs %(currency)s %(amount)s"
#: pretix/static/pretixpresale/js/ui/main.js:218
msgid "Please enter the amount the organizer can keep."
msgstr ""
msgstr "Introdueix limport que es queda lorganitzador."
#: pretix/static/pretixpresale/js/ui/main.js:570
msgid "Your local time:"
msgstr ""
msgstr "La teva hora local:"
#: pretix/static/pretixpresale/js/walletdetection.js:39
#, fuzzy
msgid "Google Pay"
msgstr ""
msgstr "Google Pay"
#: pretix/static/pretixpresale/js/widget/widget.js:16
msgctxt "widget"
msgid "Quantity"
msgstr ""
msgstr "Quantitat"
#: pretix/static/pretixpresale/js/widget/widget.js:17
msgctxt "widget"
msgid "Decrease quantity"
msgstr ""
msgstr "Disminueix quantitat"
#: pretix/static/pretixpresale/js/widget/widget.js:18
msgctxt "widget"
msgid "Increase quantity"
msgstr ""
msgstr "Augmenta quantitat"
#: pretix/static/pretixpresale/js/widget/widget.js:19
msgctxt "widget"
msgid "Filter events by"
msgstr ""
msgstr "Filtra els esdeveniments per"
#: pretix/static/pretixpresale/js/widget/widget.js:20
msgctxt "widget"
msgid "Filter"
msgstr ""
msgstr "Filtra"
#: pretix/static/pretixpresale/js/widget/widget.js:21
msgctxt "widget"
msgid "Price"
msgstr ""
msgstr "Preu"
#: pretix/static/pretixpresale/js/widget/widget.js:22
#, javascript-format
msgctxt "widget"
msgid "Original price: %s"
msgstr ""
msgstr "Preu original: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:23
#, javascript-format
msgctxt "widget"
msgid "New price: %s"
msgstr ""
msgstr "Nou preu: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:24
msgctxt "widget"
msgid "Select"
msgstr ""
msgstr "Selecciona"
#: pretix/static/pretixpresale/js/widget/widget.js:25
#, javascript-format
msgctxt "widget"
msgid "Select %s"
msgstr ""
msgstr "Selecciona %s"
#: pretix/static/pretixpresale/js/widget/widget.js:26
#, javascript-format
msgctxt "widget"
msgid "Select variant %s"
msgstr ""
msgstr "Selecciona la variant %s"
#: pretix/static/pretixpresale/js/widget/widget.js:27
msgctxt "widget"
msgid "Sold out"
msgstr ""
msgstr "Esgotat"
#: pretix/static/pretixpresale/js/widget/widget.js:28
msgctxt "widget"
msgid "Buy"
msgstr ""
msgstr "Compra"
#: pretix/static/pretixpresale/js/widget/widget.js:29
msgctxt "widget"
msgid "Register"
msgstr ""
msgstr "Registrar-vos"
#: pretix/static/pretixpresale/js/widget/widget.js:30
msgctxt "widget"
msgid "Reserved"
msgstr ""
msgstr "Reservat"
#: pretix/static/pretixpresale/js/widget/widget.js:31
msgctxt "widget"
msgid "FREE"
msgstr ""
msgstr "GRATUÏT"
#: pretix/static/pretixpresale/js/widget/widget.js:32
msgctxt "widget"
msgid "from %(currency)s %(price)s"
msgstr ""
msgstr "des de %(price)s %(currency)s"
#: pretix/static/pretixpresale/js/widget/widget.js:33
#, javascript-format
msgctxt "widget"
msgid "Image of %s"
msgstr ""
msgstr "Imatge de %s"
#: pretix/static/pretixpresale/js/widget/widget.js:34
msgctxt "widget"
msgid "incl. %(rate)s% %(taxname)s"
msgstr ""
msgstr "Inclòs %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:35
msgctxt "widget"
msgid "plus %(rate)s% %(taxname)s"
msgstr ""
msgstr "més %(rate)s% %(taxname)s"
#: pretix/static/pretixpresale/js/widget/widget.js:36
msgctxt "widget"
msgid "incl. taxes"
msgstr ""
msgstr "incl. impostos"
#: pretix/static/pretixpresale/js/widget/widget.js:37
msgctxt "widget"
msgid "plus taxes"
msgstr ""
msgstr "més impostos"
#: pretix/static/pretixpresale/js/widget/widget.js:38
#, javascript-format
msgctxt "widget"
msgid "currently available: %s"
msgstr ""
msgstr "disponibles: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:39
msgctxt "widget"
msgid "Only available with a voucher"
msgstr ""
msgstr "Només disponible amb un cupó"
#: pretix/static/pretixpresale/js/widget/widget.js:40
#: pretix/static/pretixpresale/js/widget/widget.js:43
msgctxt "widget"
msgid "Not yet available"
msgstr ""
msgstr "Encara no disponible"
#: pretix/static/pretixpresale/js/widget/widget.js:41
msgctxt "widget"
msgid "Not available anymore"
msgstr ""
msgstr "Ja no està disponible"
#: pretix/static/pretixpresale/js/widget/widget.js:42
msgctxt "widget"
msgid "Currently not available"
msgstr ""
msgstr "No disponible"
#: pretix/static/pretixpresale/js/widget/widget.js:44
#, javascript-format
msgctxt "widget"
msgid "minimum amount to order: %s"
msgstr ""
msgstr "Comanda mínima: %s"
#: pretix/static/pretixpresale/js/widget/widget.js:45
msgctxt "widget"
msgid "Close ticket shop"
msgstr ""
msgstr "Tanca la botiga d'entrades"
#: pretix/static/pretixpresale/js/widget/widget.js:46
msgctxt "widget"
msgid "The ticket shop could not be loaded."
msgstr ""
msgstr "No s'ha pogut carregar la botiga d'entrades."
#: pretix/static/pretixpresale/js/widget/widget.js:47
msgctxt "widget"

View File

@@ -5,7 +5,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-10-29 09:24+0000\n"
"PO-Revision-Date: 2025-10-30 20:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
">\n"
@@ -9664,15 +9664,15 @@ msgstr "Der Gutschein wurde an {recipient} verschickt."
#: pretix/base/settings.py:81
msgid "Compute taxes for every line individually"
msgstr ""
msgstr "Berechne Steuern für jede Position einzeln"
#: pretix/base/settings.py:82
msgid "Compute taxes based on net total"
msgstr ""
msgstr "Berechne Steuern auf Basis der Nettosumme"
#: pretix/base/settings.py:83
msgid "Compute taxes based on net total with stable gross prices"
msgstr ""
msgstr "Berechne Steuern auf Basis der Nettosumme mit konstanten Bruttopreisen"
#: pretix/base/settings.py:133
msgid "Allow usage of restricted plugins"
@@ -9762,13 +9762,8 @@ msgid "Add-on products will not be counted."
msgstr "Zusatzprodukte werden nicht mitgezählt."
#: pretix/base/settings.py:334
#, fuzzy
#| msgid ""
#| "Show net prices instead of gross prices in the product list (not "
#| "recommended!)"
msgid "Show net prices instead of gross prices in the product list"
msgstr ""
"Zeige Netto- statt Bruttopreisen in den Produktlisten (nicht empfohlen)"
msgstr "Zeigen Nettopreise statt Bruttopreisen in der Produktliste"
#: pretix/base/settings.py:335
msgid ""
@@ -9885,10 +9880,8 @@ msgid "Require a phone number per order"
msgstr "Telefonnummer pro Bestellung erfordern"
#: pretix/base/settings.py:481
#, fuzzy
#| msgid "including all taxes"
msgid "Rounding of taxes"
msgstr "inklusive aller Steuern"
msgstr "Rundung der Steuern"
#: pretix/base/settings.py:485
msgid ""
@@ -9896,6 +9889,10 @@ msgid ""
"for tax reporting, you need to make sure to account for possible rounding "
"differences if your external system rounds differently than pretix."
msgstr ""
"Bitte beachten Sie, dass bei der Übertragung der Verkaufsdaten in ein "
"externes Buchhaltungssystem darauf geachtet werden muss, dass potentielle "
"Rundungsabweichungen behandelt werden, wenn das externe System anders rundet "
"als pretix."
#: pretix/base/settings.py:500
msgid "Ask for invoice address"
@@ -14037,31 +14034,24 @@ msgstr ""
"Ausstellung ablaufen."
#: pretix/control/forms/event.py:806
#, fuzzy
#| msgid "including all taxes"
msgid "Prices including tax"
msgstr "inklusive aller Steuern"
msgstr "Preis inklusive Steuern"
#: pretix/control/forms/event.py:807
msgid "Recommended if you sell tickets at least partly to consumers."
msgstr ""
msgstr "Empfohlen, wenn mindestens teilweise an Verbraucher verkauft wird."
#: pretix/control/forms/event.py:811
#, fuzzy
#| msgctxt "reporting_timeframe"
#| msgid "All future (excluding today)"
msgid "Prices excluding tax"
msgstr "Zukunft (ab morgen)"
msgstr "Preise exklusive Steuern"
#: pretix/control/forms/event.py:812
msgid "Recommended only if you sell tickets primarily to business customers."
msgstr ""
msgstr "Empfohlen, wenn primär an Geschäftskunden verkauft wird."
#: pretix/control/forms/event.py:848
#, fuzzy
#| msgid "Date chosen by customer"
msgid "Prices shown to customer"
msgstr "Wunschdatum des Kunden"
msgstr "Preisanzeige für Kunden"
#: pretix/control/forms/event.py:852
msgid ""
@@ -14070,6 +14060,10 @@ msgid ""
"product, the total tax amount can differ from when it would be computed from "
"the order total."
msgstr ""
"Empfohlen, wenn keine E-Rechnungen benötigt werden. Jedes Produkt wird zum "
"beworbenen Netto- und Bruttopreis verkauft. Im Fall von Bestellungen mit "
"mehreren Produkten kann die Gesamt-Steuersumme höher oder niedriger sein als "
"die, die sich aus der Bestellsumme berechnen würde."
#: pretix/control/forms/event.py:857
msgid ""
@@ -14078,6 +14072,11 @@ msgid ""
"may be changed to ensure correct rounding, while the net prices will be kept "
"as configured. This may cause the actual payment amount to differ."
msgstr ""
"Empfohlen für E-Rechnungen, wenn primär an Geschäftskunden verkauft wird und "
"Kunden Nettopreise angezeigt werden. Der Bruttopreis einzelner Produkte kann "
"sich automatisch ändern, um ein korrektes Rundungsverhalten sicherzustellen, "
"während die Nettopreise immer den eingestellten Preisen entsprechen. Der "
"resultierende Zahlungsbetrag kann durch die Rundung abweichen."
#: pretix/control/forms/event.py:863
msgid ""
@@ -14087,6 +14086,13 @@ msgid ""
"configured whenever possible. Gross prices may still change if they are "
"impossible to derive from a rounded net price."
msgstr ""
"Empfohlen für E-Rechnungen, wenn primär an Verbraucher verkauft wird und "
"Kunden Bruttopreise angezeigt werden. Der Netto- oder Bruttopreis einzelner "
"Produkte kann sich automatisch ändern, um ein korrektes Rundungsverhalten "
"sicherzustellen. Dabei versucht das System, die Bruttopreise möglichst bei "
"den eingestellten Preisen zu belassen. Die Bruttopreise können sich dennoch "
"ändern, wenn sie durch Errechnung von einem gerundeten Nettopreis nicht "
"erreichbar sind."
#: pretix/control/forms/event.py:961
msgid "Generate invoices for Sales channels"
@@ -21278,6 +21284,10 @@ msgid ""
"optionally contain additional rules that depend on the customer's country "
"and type."
msgstr ""
"Steuer-Regeln definieren unterschiedliche Besteuerungsszenarien, die dann "
"den verschiedenen Produkten zugewiesen werden können. Jede Steuer-Regel "
"enthält einen Standard-Steuersatz und kann optional zusätzliche Regeln "
"enthalten, die auf die Art und Herkunft des Kunden Bezug nehmen."
#: pretix/control/templates/pretixcontrol/event/tax.html:21
msgid "You haven't created any tax rules yet."
@@ -21304,13 +21314,11 @@ msgid "Make default"
msgstr "Zum Standard machen"
#: pretix/control/templates/pretixcontrol/event/tax.html:67
#, fuzzy, python-format
#| msgid "%(count)s event"
#| msgid_plural "%(count)s events"
#, python-format
msgid "%(count)s product"
msgid_plural "%(count)s products"
msgstr[0] "%(count)s Veranstaltung"
msgstr[1] "%(count)s Veranstaltungen"
msgstr[0] "%(count)s Produkt"
msgstr[1] "%(count)s Produkte"
#: pretix/control/templates/pretixcontrol/event/tax.html:75
#, python-format
@@ -21323,16 +21331,12 @@ msgid "excl. %(rate)s %%"
msgstr "zzgl. %(rate)s %%"
#: pretix/control/templates/pretixcontrol/event/tax.html:80
#, fuzzy
#| msgid "Custom rules"
msgid "with custom rules"
msgstr "Eigene Regeln"
msgstr "mit eigenen Regeln"
#: pretix/control/templates/pretixcontrol/event/tax.html:110
#, fuzzy
#| msgid "Base settings"
msgid "Tax settings"
msgstr "Grundeinstellungen"
msgstr "Steuer-Einstellungen"
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:4
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:6
@@ -24149,7 +24153,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/transactions.html:73
#, python-format
msgid "incl. %(amount)s rounding correction"
msgstr ""
msgstr "inkl. %(amount)s Rundungskorrektur"
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:5
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:7

View File

@@ -362,6 +362,9 @@ Reverse-Proxy
Revisionssicherheit
Revolut
Revolut-App
Rundungsabweichungen
Rundungskorrektur
Rundungsverhalten
rückabgewickelt
Sa
Sammlungsstücken

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-10-29 09:24+0000\n"
"PO-Revision-Date: 2025-10-30 20:00+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
"pretix/pretix/de_Informal/>\n"
@@ -9651,15 +9651,15 @@ msgstr "Der Gutschein wurde an {recipient} verschickt."
#: pretix/base/settings.py:81
msgid "Compute taxes for every line individually"
msgstr ""
msgstr "Berechne Steuern für jede Position einzeln"
#: pretix/base/settings.py:82
msgid "Compute taxes based on net total"
msgstr ""
msgstr "Berechne Steuern auf Basis der Nettosumme"
#: pretix/base/settings.py:83
msgid "Compute taxes based on net total with stable gross prices"
msgstr ""
msgstr "Berechne Steuern auf Basis der Nettosumme mit konstanten Bruttopreisen"
#: pretix/base/settings.py:133
msgid "Allow usage of restricted plugins"
@@ -9749,13 +9749,8 @@ msgid "Add-on products will not be counted."
msgstr "Zusatzprodukte werden nicht mitgezählt."
#: pretix/base/settings.py:334
#, fuzzy
#| msgid ""
#| "Show net prices instead of gross prices in the product list (not "
#| "recommended!)"
msgid "Show net prices instead of gross prices in the product list"
msgstr ""
"Zeige Netto- statt Bruttopreisen in den Produktlisten (nicht empfohlen)"
msgstr "Zeigen Nettopreise statt Bruttopreisen in der Produktliste"
#: pretix/base/settings.py:335
msgid ""
@@ -9872,10 +9867,8 @@ msgid "Require a phone number per order"
msgstr "Telefonnummer pro Bestellung erfordern"
#: pretix/base/settings.py:481
#, fuzzy
#| msgid "including all taxes"
msgid "Rounding of taxes"
msgstr "inklusive aller Steuern"
msgstr "Rundung der Steuern"
#: pretix/base/settings.py:485
msgid ""
@@ -9883,6 +9876,10 @@ msgid ""
"for tax reporting, you need to make sure to account for possible rounding "
"differences if your external system rounds differently than pretix."
msgstr ""
"Bitte beachte, dass bei der Übertragung der Verkaufsdaten in ein externes "
"Buchhaltungssystem darauf geachtet werden muss, dass potentielle "
"Rundungsabweichungen behandelt werden, wenn das externe System anders rundet "
"als pretix."
#: pretix/base/settings.py:500
msgid "Ask for invoice address"
@@ -14013,31 +14010,24 @@ msgstr ""
"Ausstellung ablaufen."
#: pretix/control/forms/event.py:806
#, fuzzy
#| msgid "including all taxes"
msgid "Prices including tax"
msgstr "inklusive aller Steuern"
msgstr "Preis inklusive Steuern"
#: pretix/control/forms/event.py:807
msgid "Recommended if you sell tickets at least partly to consumers."
msgstr ""
msgstr "Empfohlen, wenn mindestens teilweise an Verbraucher verkauft wird."
#: pretix/control/forms/event.py:811
#, fuzzy
#| msgctxt "reporting_timeframe"
#| msgid "All future (excluding today)"
msgid "Prices excluding tax"
msgstr "Zukunft (ab morgen)"
msgstr "Preise exklusive Steuern"
#: pretix/control/forms/event.py:812
msgid "Recommended only if you sell tickets primarily to business customers."
msgstr ""
msgstr "Empfohlen, wenn primär an Geschäftskunden verkauft wird."
#: pretix/control/forms/event.py:848
#, fuzzy
#| msgid "Date chosen by customer"
msgid "Prices shown to customer"
msgstr "Wunschdatum des Kunden"
msgstr "Preisanzeige für Kunden"
#: pretix/control/forms/event.py:852
msgid ""
@@ -14046,6 +14036,10 @@ msgid ""
"product, the total tax amount can differ from when it would be computed from "
"the order total."
msgstr ""
"Empfohlen, wenn keine E-Rechnungen benötigt werden. Jedes Produkt wird zum "
"beworbenen Netto- und Bruttopreis verkauft. Im Fall von Bestellungen mit "
"mehreren Produkten kann die Gesamt-Steuersumme höher oder niedriger sein als "
"die, die sich aus der Bestellsumme berechnen würde."
#: pretix/control/forms/event.py:857
msgid ""
@@ -14054,6 +14048,11 @@ msgid ""
"may be changed to ensure correct rounding, while the net prices will be kept "
"as configured. This may cause the actual payment amount to differ."
msgstr ""
"Empfohlen für E-Rechnungen, wenn primär an Geschäftskunden verkauft wird und "
"Kunden Nettopreise angezeigt werden. Der Bruttopreis einzelner Produkte kann "
"sich automatisch ändern, um ein korrektes Rundungsverhalten sicherzustellen, "
"während die Nettopreise immer den eingestellten Preisen entsprechen. Der "
"resultierende Zahlungsbetrag kann durch die Rundung abweichen."
#: pretix/control/forms/event.py:863
msgid ""
@@ -14063,6 +14062,13 @@ msgid ""
"configured whenever possible. Gross prices may still change if they are "
"impossible to derive from a rounded net price."
msgstr ""
"Empfohlen für E-Rechnungen, wenn primär an Verbraucher verkauft wird und "
"Kunden Bruttopreise angezeigt werden. Der Netto- oder Bruttopreis einzelner "
"Produkte kann sich automatisch ändern, um ein korrektes Rundungsverhalten "
"sicherzustellen. Dabei versucht das System, die Bruttopreise möglichst bei "
"den eingestellten Preisen zu belassen. Die Bruttopreise können sich dennoch "
"ändern, wenn sie durch Errechnung von einem gerundeten Nettopreis nicht "
"erreichbar sind."
#: pretix/control/forms/event.py:961
msgid "Generate invoices for Sales channels"
@@ -21248,6 +21254,10 @@ msgid ""
"optionally contain additional rules that depend on the customer's country "
"and type."
msgstr ""
"Steuer-Regeln definieren unterschiedliche Besteuerungsszenarien, die dann "
"den verschiedenen Produkten zugewiesen werden können. Jede Steuer-Regel "
"enthält einen Standard-Steuersatz und kann optional zusätzliche Regeln "
"enthalten, die auf die Art und Herkunft des Kunden Bezug nehmen."
#: pretix/control/templates/pretixcontrol/event/tax.html:21
msgid "You haven't created any tax rules yet."
@@ -21274,13 +21284,11 @@ msgid "Make default"
msgstr "Zum Standard machen"
#: pretix/control/templates/pretixcontrol/event/tax.html:67
#, fuzzy, python-format
#| msgid "%(count)s event"
#| msgid_plural "%(count)s events"
#, python-format
msgid "%(count)s product"
msgid_plural "%(count)s products"
msgstr[0] "%(count)s Veranstaltung"
msgstr[1] "%(count)s Veranstaltungen"
msgstr[0] "%(count)s Produkt"
msgstr[1] "%(count)s Produkte"
#: pretix/control/templates/pretixcontrol/event/tax.html:75
#, python-format
@@ -21293,16 +21301,12 @@ msgid "excl. %(rate)s %%"
msgstr "zzgl. %(rate)s %%"
#: pretix/control/templates/pretixcontrol/event/tax.html:80
#, fuzzy
#| msgid "Custom rules"
msgid "with custom rules"
msgstr "Eigene Regeln"
msgstr "mit eigenen Regeln"
#: pretix/control/templates/pretixcontrol/event/tax.html:110
#, fuzzy
#| msgid "Base settings"
msgid "Tax settings"
msgstr "Grundeinstellungen"
msgstr "Steuer-Einstellungen"
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:4
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:6
@@ -24116,7 +24120,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/transactions.html:73
#, python-format
msgid "incl. %(amount)s rounding correction"
msgstr ""
msgstr "inkl. %(amount)s Rundungskorrektur"
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:5
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:7

View File

@@ -362,6 +362,9 @@ Reverse-Proxy
Revisionssicherheit
Revolut
Revolut-App
Rundungsabweichungen
Rundungskorrektur
Rundungsverhalten
rückabgewickelt
Sa
Sammlungsstücken

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-10-22 16:00+0000\n"
"PO-Revision-Date: 2025-11-04 10:25+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.13.3\n"
"X-Generator: Weblate 5.14\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -8346,10 +8346,8 @@ msgid "Event canceled"
msgstr "Evento cancelado"
#: pretix/base/services/cancelevent.py:392
#, fuzzy
#| msgid "Create configuration"
msgid "Bulk-refund confirmation"
msgstr "Crear configuración"
msgstr "Confirmación de reembolso masivo"
#: pretix/base/services/cart.py:102 pretix/base/services/modelimport.py:247
#: pretix/base/services/orders.py:155
@@ -9655,15 +9653,16 @@ msgstr "El vale de compra ha sido enviado a {recipient}."
#: pretix/base/settings.py:81
msgid "Compute taxes for every line individually"
msgstr ""
msgstr "Calcular los impuestos para cada línea individualmente"
#: pretix/base/settings.py:82
msgid "Compute taxes based on net total"
msgstr ""
msgstr "Calcular los impuestos basándose en el total neto"
#: pretix/base/settings.py:83
msgid "Compute taxes based on net total with stable gross prices"
msgstr ""
"Calcular los impuestos basándose en el total neto con precios brutos estables"
#: pretix/base/settings.py:133
msgid "Allow usage of restricted plugins"
@@ -9755,14 +9754,9 @@ msgid "Add-on products will not be counted."
msgstr "Los productos adicionales no se contarán."
#: pretix/base/settings.py:334
#, fuzzy
#| msgid ""
#| "Show net prices instead of gross prices in the product list (not "
#| "recommended!)"
msgid "Show net prices instead of gross prices in the product list"
msgstr ""
"Mostrar precios netos en lugar de precios brutos en la lista de productos "
"(¡no recomendado!)"
"Mostrar precios netos en lugar de precios brutos en la lista de productos"
#: pretix/base/settings.py:335
msgid ""
@@ -9883,10 +9877,8 @@ msgid "Require a phone number per order"
msgstr "Requerir un número de teléfono por pedido"
#: pretix/base/settings.py:481
#, fuzzy
#| msgid "including all taxes"
msgid "Rounding of taxes"
msgstr "impuestos incluidos"
msgstr "Redondeo de impuestos"
#: pretix/base/settings.py:485
msgid ""
@@ -9894,6 +9886,10 @@ msgid ""
"for tax reporting, you need to make sure to account for possible rounding "
"differences if your external system rounds differently than pretix."
msgstr ""
"Tenga en cuenta que si transfiere sus datos de ventas desde pretix a un "
"sistema externo para la declaración de impuestos, debe asegurarse de tener "
"en cuenta las posibles diferencias de redondeo si su sistema externo "
"redondea de forma diferente a pretix."
#: pretix/base/settings.py:500
msgid "Ask for invoice address"
@@ -13164,18 +13160,20 @@ msgstr ""
msgid ""
"You have requested us to cancel an event which includes a larger bulk-refund:"
msgstr ""
"Nos ha solicitado cancelar un evento que incluye un reembolso masivo de "
"mayor importe:"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt:6
#, fuzzy
#| msgid "Initiate a refund of %(amount)s"
msgid "Estimated refund amount"
msgstr "Iniciar un reembolso de %(amount)s"
msgstr "Importe estimado del reembolso"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt:8
msgid ""
"Please confirm that you want to proceed by coping the following confirmation "
"code into the cancellation form:"
msgstr ""
"Confirme que desea continuar copiando el siguiente código de confirmación en "
"el formulario de cancelación:"
#: pretix/base/templates/pretixbase/email/email_footer.html:3
#, python-format
@@ -14017,31 +14015,25 @@ msgstr ""
"año en el que se emite la tarjeta de regalo."
#: pretix/control/forms/event.py:806
#, fuzzy
#| msgid "including all taxes"
msgid "Prices including tax"
msgstr "impuestos incluidos"
msgstr "Precios con impuestos incluidos"
#: pretix/control/forms/event.py:807
msgid "Recommended if you sell tickets at least partly to consumers."
msgstr ""
msgstr "Recomendado si se venden entradas, al menos en parte, a consumidores."
#: pretix/control/forms/event.py:811
#, fuzzy
#| msgctxt "reporting_timeframe"
#| msgid "All future (excluding today)"
msgid "Prices excluding tax"
msgstr "Todo el futuro (excepto hoy)"
msgstr "Precios sin impuestos"
#: pretix/control/forms/event.py:812
msgid "Recommended only if you sell tickets primarily to business customers."
msgstr ""
"Recomendado solo si vende entradas principalmente a clientes empresariales."
#: pretix/control/forms/event.py:848
#, fuzzy
#| msgid "Date chosen by customer"
msgid "Prices shown to customer"
msgstr "Fecha elegida por el cliente"
msgstr "Precios mostrados a los clientes"
#: pretix/control/forms/event.py:852
msgid ""
@@ -14050,6 +14042,10 @@ msgid ""
"product, the total tax amount can differ from when it would be computed from "
"the order total."
msgstr ""
"Recomendado cuando no se requiere facturación electrónica. Cada producto se "
"venderá con el precio neto y bruto anunciado. Sin embargo, en pedidos de más "
"de un producto, el importe total de los impuestos puede diferir del que se "
"calcularía a partir del total del pedido."
#: pretix/control/forms/event.py:857
msgid ""
@@ -14058,6 +14054,12 @@ msgid ""
"may be changed to ensure correct rounding, while the net prices will be kept "
"as configured. This may cause the actual payment amount to differ."
msgstr ""
"Recomendado para la facturación electrónica cuando se vende principalmente a "
"clientes empresariales y se muestran los precios a los clientes sin "
"impuestos. El precio bruto de algunos productos puede modificarse para "
"garantizar un redondeo correcto, mientras que los precios netos se "
"mantendrán tal y como están configurados. Esto puede provocar que el importe "
"real del pago sea diferente."
#: pretix/control/forms/event.py:863
msgid ""
@@ -14067,6 +14069,12 @@ msgid ""
"configured whenever possible. Gross prices may still change if they are "
"impossible to derive from a rounded net price."
msgstr ""
"Recomendado para la facturación electrónica cuando se vende principalmente a "
"consumidores. El precio bruto o neto de algunos productos puede modificarse "
"automáticamente para garantizar el redondeo correcto del total del pedido. "
"El sistema intenta mantener los precios brutos tal y como se han configurado "
"siempre que sea posible. Los precios brutos pueden seguir cambiando si no es "
"posible obtenerlos a partir de un precio neto redondeado."
#: pretix/control/forms/event.py:961
msgid "Generate invoices for Sales channels"
@@ -16185,7 +16193,7 @@ msgstr "Confirme que desea cancelar TODAS las fechas de esta serie de eventos."
#: pretix/control/forms/orders.py:1037
msgid "I understand that this is not reversible and want to continue"
msgstr ""
msgstr "Entiendo que esto no es reversible y deseo continuar"
#: pretix/control/forms/orders.py:1041
#: pretix/control/templates/pretixcontrol/shredder/download.html:53
@@ -16196,12 +16204,12 @@ msgstr "Código de confirmación"
msgid ""
"We have just emailed you a confirmation code to enter to confirm this action"
msgstr ""
"Acabamos de enviarle por correo electrónico un código de confirmación que "
"deberá introducir para confirmar esta acción"
#: pretix/control/forms/orders.py:1055
#, fuzzy
#| msgid "The confirm code you entered was incorrect."
msgid "The confirmation code is incorrect."
msgstr "El código de confirmación que introdujo era incorrecto."
msgstr "El código de confirmación es incorrecto."
#: pretix/control/forms/organizer.py:93
msgid "This slug is already in use. Please choose a different one."
@@ -21268,6 +21276,10 @@ msgid ""
"optionally contain additional rules that depend on the customer's country "
"and type."
msgstr ""
"Las normas fiscales definen diferentes escenarios fiscales que luego se "
"pueden asignar a los productos individuales. Cada norma fiscal contiene un "
"tipo impositivo predeterminado y, opcionalmente, puede contener normas "
"adicionales que dependen del país y el tipo de cliente."
#: pretix/control/templates/pretixcontrol/event/tax.html:21
msgid "You haven't created any tax rules yet."
@@ -21294,13 +21306,11 @@ msgid "Make default"
msgstr "Establecer por defecto"
#: pretix/control/templates/pretixcontrol/event/tax.html:67
#, fuzzy, python-format
#| msgid "%(count)s event"
#| msgid_plural "%(count)s events"
#, python-format
msgid "%(count)s product"
msgid_plural "%(count)s products"
msgstr[0] "%(count)s elemento"
msgstr[1] "%(count)s elementos"
msgstr[0] "%(count)s producto"
msgstr[1] "%(count)s productos"
#: pretix/control/templates/pretixcontrol/event/tax.html:75
#, python-format
@@ -21313,16 +21323,12 @@ msgid "excl. %(rate)s %%"
msgstr "excluido %(rate)s %%"
#: pretix/control/templates/pretixcontrol/event/tax.html:80
#, fuzzy
#| msgid "Custom rules"
msgid "with custom rules"
msgstr "Reglas personalizadas"
msgstr "con reglas personalizadas"
#: pretix/control/templates/pretixcontrol/event/tax.html:110
#, fuzzy
#| msgid "Base settings"
msgid "Tax settings"
msgstr "Ajustes básicos"
msgstr "Configuración de impuestos"
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:4
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:6
@@ -24141,7 +24147,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/transactions.html:73
#, python-format
msgid "incl. %(amount)s rounding correction"
msgstr ""
msgstr "incl. %(amount)s corrección por redondeo"
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:5
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:7
@@ -24247,84 +24253,77 @@ msgstr ""
"los que estén en la lista de espera recibirán varios correos electrónicos."
#: pretix/control/templates/pretixcontrol/orders/cancel.html:84
#, fuzzy
#| msgid "Refund amount"
msgid "Preview refund amount"
msgstr "Importe del reembolso"
msgstr "Previsualizar el importe del reembolso"
#: pretix/control/templates/pretixcontrol/orders/cancel.html:88
msgid "Cancel all orders"
msgstr "Cancelar todos los pedidos"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:13
#, fuzzy
#| msgid "If you contact us, please send us the following code:"
msgid "If you proceed, the system will do the following:"
msgstr ""
"Si se pone en contacto con nosotros, por favor envíenos el siguiente código:"
msgstr "Si continúa, el sistema hará lo siguiente:"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:19
#, python-format
msgid "%(count)s order will be canceled fully"
msgid_plural "%(count)s orders will be canceled fully"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "%(count)s pedido se cancelará por completo"
msgstr[1] "%(count)s pedidos se cancelarán por completo"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:26
#, python-format
msgid "%(count)s order will be canceled partially"
msgid_plural "%(count)s orders will be canceled partially"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "%(count)s pedido se cancelará parcialmente"
msgstr[1] "%(count)s pedidos se cancelarán parcialmente"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:36
#, python-format
msgid "%(amount)s are eligible for refunds."
msgstr ""
msgstr "%(amount)s son reembolsables."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:40
msgid ""
"The system will attempt to refund the money automatically if supported by "
"the payment method."
msgstr ""
"El sistema intentará reembolsar el dinero automáticamente si el método de "
"pago lo permite."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:42
msgid "The system will create manual refunds that you need to execute."
msgstr ""
msgstr "El sistema creará reembolsos manuales que deberá ejecutar."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:44
#, fuzzy
#| msgid "Text (if order will not expire automatically)"
msgid "Refunds will not happen automatically."
msgstr "Texto (si el pedido no caducará automáticamente)"
msgstr "Los reembolsos no se realizarán automáticamente."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:52
#, fuzzy
#| msgid "Send information via email"
msgid "Inform all customers via email."
msgstr "Enviar información por correo electrónico"
msgstr "Informar a todos los clientes por correo electrónico."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:57
msgid "Inform all waiting list contacts via email."
msgstr ""
"Informar a todos los contactos de la lista de espera por correo electrónico."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:62
msgid ""
"These numbers are estimates and may change if the data in your event "
"recently changed."
msgstr ""
"Estas cifras son estimaciones y pueden variar si los datos del evento han "
"cambiado recientemente."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:76
#, fuzzy, python-format
#| msgid "Initiate a refund of %(amount)s"
#, python-format
msgid "Proceed and refund approx. %(amount)s"
msgstr "Iniciar un reembolso de %(amount)s"
msgstr "Proseguir y reembolsar aprox. %(amount)s"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:80
#, fuzzy
#| msgid "Yes, cancel order"
msgid "Proceed and cancel orders"
msgstr "Sí, cancelar pedido"
msgstr "Proceder y cancelar pedidos"
#: pretix/control/templates/pretixcontrol/orders/export.html:5
#: pretix/control/templates/pretixcontrol/orders/export.html:8

View File

@@ -4,7 +4,7 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-10-22 16:00+0000\n"
"PO-Revision-Date: 2025-11-04 10:25+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
@@ -13,7 +13,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.13.3\n"
"X-Generator: Weblate 5.14\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -8397,10 +8397,8 @@ msgid "Event canceled"
msgstr "Événement annulé"
#: pretix/base/services/cancelevent.py:392
#, fuzzy
#| msgid "Create configuration"
msgid "Bulk-refund confirmation"
msgstr "Créer une configuration"
msgstr "Confirmation de remboursement en bloc"
#: pretix/base/services/cart.py:102 pretix/base/services/modelimport.py:247
#: pretix/base/services/orders.py:155
@@ -9720,15 +9718,15 @@ msgstr "Le bon a été envoyé à {recipient}."
#: pretix/base/settings.py:81
msgid "Compute taxes for every line individually"
msgstr ""
msgstr "Calculer les taxes individuellement pour chaque ligne"
#: pretix/base/settings.py:82
msgid "Compute taxes based on net total"
msgstr ""
msgstr "Calculer les taxes sur la base du total net"
#: pretix/base/settings.py:83
msgid "Compute taxes based on net total with stable gross prices"
msgstr ""
msgstr "Calculer les taxes sur la base du total net avec des prix bruts stables"
#: pretix/base/settings.py:133
msgid "Allow usage of restricted plugins"
@@ -9821,14 +9819,9 @@ msgid "Add-on products will not be counted."
msgstr "Les Add-Ons e sont pas comptabilisés."
#: pretix/base/settings.py:334
#, fuzzy
#| msgid ""
#| "Show net prices instead of gross prices in the product list (not "
#| "recommended!)"
msgid "Show net prices instead of gross prices in the product list"
msgstr ""
"Afficher les prix nets au lieu des prix bruts dans la liste de produits (pas "
"recommandé!)"
"Afficher les prix nets au lieu des prix bruts dans la liste des produits"
#: pretix/base/settings.py:335
msgid ""
@@ -9947,10 +9940,8 @@ msgid "Require a phone number per order"
msgstr "Exiger un numéro de téléphone par commande"
#: pretix/base/settings.py:481
#, fuzzy
#| msgid "including all taxes"
msgid "Rounding of taxes"
msgstr "toutes taxes comprises"
msgstr "Arrondi des taxes"
#: pretix/base/settings.py:485
msgid ""
@@ -9958,6 +9949,10 @@ msgid ""
"for tax reporting, you need to make sure to account for possible rounding "
"differences if your external system rounds differently than pretix."
msgstr ""
"Veuillez noter que si vous transférez vos données de vente depuis pretix "
"vers un système externe à des fins de déclaration fiscale, vous devez tenir "
"compte des éventuelles différences d'arrondi si votre système externe "
"arrondit différemment de pretix."
#: pretix/base/settings.py:500
msgid "Ask for invoice address"
@@ -13290,18 +13285,20 @@ msgstr ""
msgid ""
"You have requested us to cancel an event which includes a larger bulk-refund:"
msgstr ""
"Vous nous avez demandé d'annuler un événement qui implique un remboursement "
"groupé important :"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt:6
#, fuzzy
#| msgid "Initiate a refund of %(amount)s"
msgid "Estimated refund amount"
msgstr "Initier un remboursement de %(amount)s"
msgstr "Montant estimé du remboursement"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt:8
msgid ""
"Please confirm that you want to proceed by coping the following confirmation "
"code into the cancellation form:"
msgstr ""
"Veuillez confirmer que vous souhaitez poursuivre en copiant le code de "
"confirmation suivant dans le formulaire d'annulation :"
#: pretix/base/templates/pretixbase/email/email_footer.html:3
#, python-format
@@ -14151,31 +14148,27 @@ msgstr ""
"plus lannée démission de la carte-cadeau."
#: pretix/control/forms/event.py:806
#, fuzzy
#| msgid "including all taxes"
msgid "Prices including tax"
msgstr "toutes taxes comprises"
msgstr "Prix TTC"
#: pretix/control/forms/event.py:807
msgid "Recommended if you sell tickets at least partly to consumers."
msgstr ""
"Recommandé si vous vendez des billets au moins en partie à des consommateurs."
#: pretix/control/forms/event.py:811
#, fuzzy
#| msgctxt "reporting_timeframe"
#| msgid "All future (excluding today)"
msgid "Prices excluding tax"
msgstr "Tout futur (sauf aujourdhui)"
msgstr "Prix hors taxes"
#: pretix/control/forms/event.py:812
msgid "Recommended only if you sell tickets primarily to business customers."
msgstr ""
"Recommandé uniquement si vous vendez principalement des billets à des "
"clients professionnels."
#: pretix/control/forms/event.py:848
#, fuzzy
#| msgid "Date chosen by customer"
msgid "Prices shown to customer"
msgstr "Date choisie par le client"
msgstr "Prix indiqués aux clients"
#: pretix/control/forms/event.py:852
msgid ""
@@ -14184,6 +14177,11 @@ msgid ""
"product, the total tax amount can differ from when it would be computed from "
"the order total."
msgstr ""
"Recommandé lorsque la facturation électronique n'est pas requise. Chaque "
"produit sera vendu au prix net et brut annoncé. Cependant, dans le cas de "
"commandes comprenant plusieurs produits, le montant total des taxes peut "
"différer de celui qui serait calculé à partir du montant total de la "
"commande."
#: pretix/control/forms/event.py:857
msgid ""
@@ -14192,6 +14190,12 @@ msgid ""
"may be changed to ensure correct rounding, while the net prices will be kept "
"as configured. This may cause the actual payment amount to differ."
msgstr ""
"Recommandé pour la facturation électronique lorsque vous vendez "
"principalement à des clients professionnels et affichez les prix hors taxes "
"à vos clients. Le prix brut de certains produits peut être modifié afin "
"d'assurer un arrondi correct, tandis que les prix nets resteront tels qu'ils "
"ont été configurés. Cela peut entraîner une différence entre le montant réel "
"du paiement et le montant indiqué."
#: pretix/control/forms/event.py:863
msgid ""
@@ -14201,6 +14205,13 @@ msgid ""
"configured whenever possible. Gross prices may still change if they are "
"impossible to derive from a rounded net price."
msgstr ""
"Recommandé pour la facturation électronique lorsque vous vendez "
"principalement à des consommateurs. Le prix brut ou net de certains produits "
"peut être modifié automatiquement afin d'assurer un arrondi correct du total "
"de la commande. Le système tente de conserver les prix bruts tels qu'ils ont "
"été configurés dans la mesure du possible. Les prix bruts peuvent toutefois "
"être modifiés s'il est impossible de les calculer à partir d'un prix net "
"arrondi."
#: pretix/control/forms/event.py:961
msgid "Generate invoices for Sales channels"
@@ -16332,6 +16343,7 @@ msgstr ""
#: pretix/control/forms/orders.py:1037
msgid "I understand that this is not reversible and want to continue"
msgstr ""
"Je comprends que cette opération est irréversible et je souhaite continuer"
#: pretix/control/forms/orders.py:1041
#: pretix/control/templates/pretixcontrol/shredder/download.html:53
@@ -16342,12 +16354,12 @@ msgstr "Code de confirmation"
msgid ""
"We have just emailed you a confirmation code to enter to confirm this action"
msgstr ""
"Nous venons de vous envoyer par e-mail un code de confirmation à saisir pour "
"confirmer cette action"
#: pretix/control/forms/orders.py:1055
#, fuzzy
#| msgid "The confirm code you entered was incorrect."
msgid "The confirmation code is incorrect."
msgstr "Le code de confirmation que vous avez entré était incorrect."
msgstr "Le code de confirmation est incorrect."
#: pretix/control/forms/organizer.py:93
msgid "This slug is already in use. Please choose a different one."
@@ -21419,6 +21431,10 @@ msgid ""
"optionally contain additional rules that depend on the customer's country "
"and type."
msgstr ""
"Les règles fiscales définissent différents scénarios d'imposition qui "
"peuvent ensuite être attribués à chaque produit. Chaque règle fiscale "
"contient un taux d'imposition par défaut et peut éventuellement contenir des "
"règles supplémentaires qui dépendent du pays et du type de client."
#: pretix/control/templates/pretixcontrol/event/tax.html:21
msgid "You haven't created any tax rules yet."
@@ -21445,13 +21461,11 @@ msgid "Make default"
msgstr "Définir par défaut"
#: pretix/control/templates/pretixcontrol/event/tax.html:67
#, fuzzy, python-format
#| msgid "%(count)s event"
#| msgid_plural "%(count)s events"
#, python-format
msgid "%(count)s product"
msgid_plural "%(count)s products"
msgstr[0] "%(count)s élement"
msgstr[1] "%(count)s élements"
msgstr[0] "%(count)s produit"
msgstr[1] "%(count)s produits"
#: pretix/control/templates/pretixcontrol/event/tax.html:75
#, python-format
@@ -21464,16 +21478,12 @@ msgid "excl. %(rate)s %%"
msgstr "hors %(rate)s %%"
#: pretix/control/templates/pretixcontrol/event/tax.html:80
#, fuzzy
#| msgid "Custom rules"
msgid "with custom rules"
msgstr "Règles personnalisées"
msgstr "avec des règles personnalisées"
#: pretix/control/templates/pretixcontrol/event/tax.html:110
#, fuzzy
#| msgid "Base settings"
msgid "Tax settings"
msgstr "Réglages de base"
msgstr "Paramètres relatifs aux taxes"
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:4
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:6
@@ -24314,7 +24324,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/transactions.html:73
#, python-format
msgid "incl. %(amount)s rounding correction"
msgstr ""
msgstr "y compris %(amount)s de correction d'arrondi"
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:5
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:7
@@ -24425,83 +24435,77 @@ msgstr ""
"plusieurs courriels."
#: pretix/control/templates/pretixcontrol/orders/cancel.html:84
#, fuzzy
#| msgid "Refund amount"
msgid "Preview refund amount"
msgstr "Montant du remboursement"
msgstr "Prévisualiser le montant du remboursement"
#: pretix/control/templates/pretixcontrol/orders/cancel.html:88
msgid "Cancel all orders"
msgstr "Annuler toutes les commandes"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:13
#, fuzzy
#| msgid "If you contact us, please send us the following code:"
msgid "If you proceed, the system will do the following:"
msgstr "Si vous nous contactez, veuillez nous envoyer le code suivant:"
msgstr "Si vous continuez, le système effectuera les opérations suivantes :"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:19
#, python-format
msgid "%(count)s order will be canceled fully"
msgid_plural "%(count)s orders will be canceled fully"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "%(count)s commande sera totalement annulée"
msgstr[1] "%(count)s commandes seront totalement annulées"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:26
#, python-format
msgid "%(count)s order will be canceled partially"
msgid_plural "%(count)s orders will be canceled partially"
msgstr[0] ""
msgstr[1] ""
msgstr[0] "%(count)s commande sera partiellement annulé"
msgstr[1] "%(count)s commandes seront partiellement annulées"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:36
#, python-format
msgid "%(amount)s are eligible for refunds."
msgstr ""
msgstr "%(amount)s sont susceptibles d'être remboursés."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:40
msgid ""
"The system will attempt to refund the money automatically if supported by "
"the payment method."
msgstr ""
"Le système tentera de rembourser automatiquement l'argent si le mode de "
"paiement le permet."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:42
msgid "The system will create manual refunds that you need to execute."
msgstr ""
msgstr "Le système créera des remboursements manuels que vous devrez exécuter."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:44
#, fuzzy
#| msgid "Text (if order will not expire automatically)"
msgid "Refunds will not happen automatically."
msgstr "Texte (si la commande nexpire pas automatiquement)"
msgstr "Les remboursements ne se feront pas automatiquement."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:52
#, fuzzy
#| msgid "Send information via email"
msgid "Inform all customers via email."
msgstr "Envoyer des informations par e-mail"
msgstr "Informez tous les clients par e-mail."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:57
msgid "Inform all waiting list contacts via email."
msgstr ""
"Informez toutes les personnes inscrites sur la liste d'attente par e-mail."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:62
msgid ""
"These numbers are estimates and may change if the data in your event "
"recently changed."
msgstr ""
"Ces chiffres sont des estimations et peuvent changer si les données "
"relatives à votre événement ont récemment été modifiées."
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:76
#, fuzzy, python-format
#| msgid "Initiate a refund of %(amount)s"
#, python-format
msgid "Proceed and refund approx. %(amount)s"
msgstr "Initier un remboursement de %(amount)s"
msgstr "Poursuivre et rembourser environ %(amount)s"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:80
#, fuzzy
#| msgid "Yes, cancel order"
msgid "Proceed and cancel orders"
msgstr "Oui, annuler la commande"
msgstr "Poursuivre et annuler les commandes"
#: pretix/control/templates/pretixcontrol/orders/export.html:5
#: pretix/control/templates/pretixcontrol/orders/export.html:8

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-30 10:55+0000\n"
"PO-Revision-Date: 2025-10-27 12:00+0000\n"
"PO-Revision-Date: 2025-11-04 10:25+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
@@ -8190,10 +8190,8 @@ msgid "Event canceled"
msgstr "イベントがキャンセルされました"
#: pretix/base/services/cancelevent.py:392
#, fuzzy
#| msgid "Add confirmation text"
msgid "Bulk-refund confirmation"
msgstr "確認テキストを追加"
msgstr "一括払い戻しの確認"
#: pretix/base/services/cart.py:102 pretix/base/services/modelimport.py:247
#: pretix/base/services/orders.py:155
@@ -9431,15 +9429,15 @@ msgstr "バウチャーは、{recipient}あてに送信されました。"
#: pretix/base/settings.py:81
msgid "Compute taxes for every line individually"
msgstr ""
msgstr "各行ごとに税額を計算する"
#: pretix/base/settings.py:82
msgid "Compute taxes based on net total"
msgstr ""
msgstr "税抜合計に基づいて税額を計算する"
#: pretix/base/settings.py:83
msgid "Compute taxes based on net total with stable gross prices"
msgstr ""
msgstr "税込価格を固定して税抜合計に基づいて税額を計算する"
#: pretix/base/settings.py:133
msgid "Allow usage of restricted plugins"
@@ -9527,12 +9525,8 @@ msgid "Add-on products will not be counted."
msgstr "アドオン製品はカウントされません。"
#: pretix/base/settings.py:334
#, fuzzy
#| msgid ""
#| "Show net prices instead of gross prices in the product list (not "
#| "recommended!)"
msgid "Show net prices instead of gross prices in the product list"
msgstr "製品リストには、総額ではなく純額を表示してください(お勧めしません!)"
msgstr "製品リストに税込価格ではなく税抜価格を表示する"
#: pretix/base/settings.py:335
msgid ""
@@ -9646,10 +9640,8 @@ msgid "Require a phone number per order"
msgstr "1注文につき1つの電話番号が必要"
#: pretix/base/settings.py:481
#, fuzzy
#| msgid "including all taxes"
msgid "Rounding of taxes"
msgstr "すべての税金を含めて"
msgstr "税額の端数処理"
#: pretix/base/settings.py:485
msgid ""
@@ -9657,6 +9649,9 @@ msgid ""
"for tax reporting, you need to make sure to account for possible rounding "
"differences if your external system rounds differently than pretix."
msgstr ""
"pretixから外部システムに販売データを転送して税務申告を行う場合、外部システム"
"の端数処理がpretixと異なる場合は、端数処理の差異を考慮する必要があることに注"
"意してください。"
#: pretix/base/settings.py:500
msgid "Ask for invoice address"
@@ -12791,19 +12786,18 @@ msgstr "数分以上かかる場合は、このページを更新するか、お
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt:2
msgid ""
"You have requested us to cancel an event which includes a larger bulk-refund:"
msgstr ""
msgstr "大量の払い戻しを伴うイベントのキャンセルをリクエストされました:"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt:6
#, fuzzy
#| msgid "Initiate a refund of %(amount)s"
msgid "Estimated refund amount"
msgstr "%(amount)sの払い戻しを開始する"
msgstr "推定払い戻し金額"
#: pretix/base/templates/pretixbase/email/cancel_confirm.txt:8
msgid ""
"Please confirm that you want to proceed by coping the following confirmation "
"code into the cancellation form:"
msgstr ""
msgstr "以下の確認コードをキャンセルフォームにコピーして、続行することを確認してくだ"
"さい:"
#: pretix/base/templates/pretixbase/email/email_footer.html:3
#, python-format
@@ -13624,31 +13618,24 @@ msgid ""
msgstr "ギフトカードの有効期限を発行年を含めた{}年に設定しました。"
#: pretix/control/forms/event.py:806
#, fuzzy
#| msgid "including all taxes"
msgid "Prices including tax"
msgstr "すべての税金を含めて"
msgstr "税込価格"
#: pretix/control/forms/event.py:807
msgid "Recommended if you sell tickets at least partly to consumers."
msgstr ""
msgstr "少なくとも一部を消費者に販売する場合に推奨されます。"
#: pretix/control/forms/event.py:811
#, fuzzy
#| msgctxt "reporting_timeframe"
#| msgid "All future (excluding today)"
msgid "Prices excluding tax"
msgstr "すべての未来(今日を除く)"
msgstr "税込価格"
#: pretix/control/forms/event.py:812
msgid "Recommended only if you sell tickets primarily to business customers."
msgstr ""
msgstr "主に法人顧客にチケットを販売する場合のみ推奨されます。"
#: pretix/control/forms/event.py:848
#, fuzzy
#| msgid "Date chosen by customer"
msgid "Prices shown to customer"
msgstr "お客様が選んだ日付"
msgstr "顧客に表示される価格"
#: pretix/control/forms/event.py:852
msgid ""
@@ -13657,6 +13644,9 @@ msgid ""
"product, the total tax amount can differ from when it would be computed from "
"the order total."
msgstr ""
"電子インボイスが不要な場合に推奨されます。各製品は表示された税抜価格と税込価"
"格で販売されます。ただし、複数の製品を含む注文では、合計税額が注文合計から計"
"算した場合と異なる可能性があります。"
#: pretix/control/forms/event.py:857
msgid ""
@@ -13665,6 +13655,10 @@ msgid ""
"may be changed to ensure correct rounding, while the net prices will be kept "
"as configured. This may cause the actual payment amount to differ."
msgstr ""
"主に法人顧客に販売し、顧客に税抜価格を表示する場合の電子インボイスに推奨され"
"ます。正確な端数処理を確保するために、一部の製品の税込価格が変更される場合が"
"ありますが、税抜価格は設定どおりに保たれます。これにより実際の支払い金額が異"
"なる場合があります。"
#: pretix/control/forms/event.py:863
msgid ""
@@ -13674,6 +13668,11 @@ msgid ""
"configured whenever possible. Gross prices may still change if they are "
"impossible to derive from a rounded net price."
msgstr ""
"主に消費者に販売する場合の電子インボイスに推奨されます。注文合計の正確な端数"
"処理を確保するために、一部の製品の税込価格または税抜価格が自動的に変更される"
"場合があります。システムは可能な限り設定どおりの税込価格を維持しようとします"
"。ただし、端数処理された税抜価格から算出できない場合は、税込価格が変更される"
"ことがあります。"
#: pretix/control/forms/event.py:961
msgid "Generate invoices for Sales channels"
@@ -14127,7 +14126,7 @@ msgstr "有効な注文"
#: pretix/control/forms/filter.py:220
msgid "Paid (or canceled with paid fee)"
msgstr "支払われた(または支払われた手数料でキャンセルされた"
msgstr "支払い済み (または支払い済み手数料でキャンセルされた)"
#: pretix/control/forms/filter.py:221
#: pretix/control/templates/pretixcontrol/items/question.html:27
@@ -14154,7 +14153,7 @@ msgstr "キャンセル"
#: pretix/control/forms/filter.py:226
msgid "Canceled (fully)"
msgstr "キャンセルされました(完全に)"
msgstr "キャンセル済み (全額)"
#: pretix/control/forms/filter.py:227
msgid "Canceled (fully or with paid fee)"
@@ -14166,11 +14165,11 @@ msgstr "キャンセル済み少なくとも1つのポジション"
#: pretix/control/forms/filter.py:229
msgid "Cancellation requested"
msgstr "キャンセルがリクエストされました"
msgstr "キャンセル申請済み"
#: pretix/control/forms/filter.py:230
msgid "Fully canceled but invoice not canceled"
msgstr "完全にキャンセルされましたが、請求書はキャンセルされていません"
msgstr "キャンセル済み(請求書はキャンセル)"
#: pretix/control/forms/filter.py:232
msgid "Payment process"
@@ -14188,11 +14187,11 @@ msgstr "保留中(期限切れ)"
#: pretix/control/forms/filter.py:236
msgid "Overpaid"
msgstr "過剰な給料"
msgstr "過払い済み"
#: pretix/control/forms/filter.py:237
msgid "Partially paid"
msgstr "部分的に支払われました"
msgstr "一部支払い済み"
#: pretix/control/forms/filter.py:238
msgid "Underpaid (but confirmed)"
@@ -15748,7 +15747,7 @@ msgstr ""
#: pretix/control/forms/orders.py:1037
msgid "I understand that this is not reversible and want to continue"
msgstr ""
msgstr "これは元に戻せないことを理解し、続行します"
#: pretix/control/forms/orders.py:1041
#: pretix/control/templates/pretixcontrol/shredder/download.html:53
@@ -15758,13 +15757,11 @@ msgstr "確認コード"
#: pretix/control/forms/orders.py:1042
msgid ""
"We have just emailed you a confirmation code to enter to confirm this action"
msgstr ""
msgstr "この操作を確認するための確認コードをメールで送信しました"
#: pretix/control/forms/orders.py:1055
#, fuzzy
#| msgid "The confirm code you entered was incorrect."
msgid "The confirmation code is incorrect."
msgstr "入力した確認コードが間違っています。"
msgstr "確認コードが正しくありません。"
#: pretix/control/forms/organizer.py:93
msgid "This slug is already in use. Please choose a different one."
@@ -20693,6 +20690,9 @@ msgid ""
"optionally contain additional rules that depend on the customer's country "
"and type."
msgstr ""
"税ルールは、個々の製品に割り当てることができるさまざまな課税シナリオを定義し"
"ます。各税ルールには既定の税率が含まれ、オプションで顧客の国や種別に応じた追"
"加ルールを含めることができます。"
#: pretix/control/templates/pretixcontrol/event/tax.html:21
msgid "You haven't created any tax rules yet."
@@ -20719,12 +20719,10 @@ msgid "Make default"
msgstr "デフォルトに設定"
#: pretix/control/templates/pretixcontrol/event/tax.html:67
#, fuzzy, python-format
#| msgid "%(count)s event"
#| msgid_plural "%(count)s events"
#, python-format
msgid "%(count)s product"
msgid_plural "%(count)s products"
msgstr[0] "%(count)s 件のイベント"
msgstr[0] "%(count)s個の製品"
#: pretix/control/templates/pretixcontrol/event/tax.html:75
#, python-format
@@ -20737,16 +20735,12 @@ msgid "excl. %(rate)s %%"
msgstr "%(rate)s %%を除く"
#: pretix/control/templates/pretixcontrol/event/tax.html:80
#, fuzzy
#| msgid "Custom rules"
msgid "with custom rules"
msgstr ""
msgstr "カスタムルールあり"
#: pretix/control/templates/pretixcontrol/event/tax.html:110
#, fuzzy
#| msgid "Base settings"
msgid "Tax settings"
msgstr "ベースの設定"
msgstr "の設定"
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:4
#: pretix/control/templates/pretixcontrol/event/tax_delete.html:6
@@ -23487,7 +23481,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/order/transactions.html:73
#, python-format
msgid "incl. %(amount)s rounding correction"
msgstr ""
msgstr "%(amount)sの端数処理調整を含む"
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:5
#: pretix/control/templates/pretixcontrol/orders/bulk_action.html:7
@@ -23590,81 +23584,71 @@ msgstr ""
"空席待ちリストのすべての人が複数のメールを受け取ることになります。"
#: pretix/control/templates/pretixcontrol/orders/cancel.html:84
#, fuzzy
#| msgid "Refund amount"
msgid "Preview refund amount"
msgstr "払い戻し金額"
msgstr "払い戻し金額のプレビュー"
#: pretix/control/templates/pretixcontrol/orders/cancel.html:88
msgid "Cancel all orders"
msgstr "すべての注文をキャンセルします"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:13
#, fuzzy
#| msgid "If you contact us, please send us the following code:"
msgid "If you proceed, the system will do the following:"
msgstr "お問い合わせの際は、以下のコードをお送りください:"
msgstr "続行すると、システムは以下を実行します:"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:19
#, python-format
msgid "%(count)s order will be canceled fully"
msgid_plural "%(count)s orders will be canceled fully"
msgstr[0] ""
msgstr[0] "%(count)s件の注文が全額キャンセルされます"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:26
#, python-format
msgid "%(count)s order will be canceled partially"
msgid_plural "%(count)s orders will be canceled partially"
msgstr[0] ""
msgstr[0] "%(count)s件の注文が一部キャンセルされます"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:36
#, python-format
msgid "%(amount)s are eligible for refunds."
msgstr ""
msgstr "%(amount)sが払い戻し対象です。"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:40
msgid ""
"The system will attempt to refund the money automatically if supported by "
"the payment method."
msgstr ""
msgstr "当該の決済方法に対応している場合、システムは自動的に払い戻しを試みます。"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:42
msgid "The system will create manual refunds that you need to execute."
msgstr ""
msgstr "システムは手動払い戻しを作成しますので、ご自身で実行する必要があります。"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:44
#, fuzzy
#| msgid "Text (if order will not expire automatically)"
msgid "Refunds will not happen automatically."
msgstr "テキスト(注文が自動的に期限切れにならない場合)"
msgstr "払い戻しは自動的には実行されません。"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:52
#, fuzzy
#| msgid "Send information via email"
msgid "Inform all customers via email."
msgstr "メールで情報を送信する"
msgstr "メールで情報を送信する"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:57
msgid "Inform all waiting list contacts via email."
msgstr ""
msgstr "すべての空席待ちリストの連絡先にメールで通知する。"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:62
msgid ""
"These numbers are estimates and may change if the data in your event "
"recently changed."
msgstr ""
msgstr "これらの数値は推定値であり、イベントのデータが最近変更された場合は変わる可能"
"性があります。"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:76
#, fuzzy, python-format
#| msgid "Initiate a refund of %(amount)s"
#, python-format
msgid "Proceed and refund approx. %(amount)s"
msgstr "%(amount)s払い戻しを開始する"
msgstr "続行して約%(amount)s払い戻"
#: pretix/control/templates/pretixcontrol/orders/cancel_confirm.html:80
#, fuzzy
#| msgid "Yes, cancel order"
msgid "Proceed and cancel orders"
msgstr "はい、注文をキャンセルします"
msgstr "続行して注文をキャンセルす"
#: pretix/control/templates/pretixcontrol/orders/export.html:5
#: pretix/control/templates/pretixcontrol/orders/export.html:8
@@ -24059,7 +24043,7 @@ msgstr "請求書はキャンセルされていません"
#: pretix/control/templates/pretixcontrol/orders/index.html:251
msgid "Sum over all pages"
msgstr "すべてのページに渡り合計する"
msgstr "全ページの合計"
#: pretix/control/templates/pretixcontrol/orders/index.html:254
#, python-format
@@ -30213,7 +30197,7 @@ msgstr "手動処理用の完全にカスタマイズ可能な支払い方法。
#: pretix/plugins/paypal2/payment.py:1097
#: pretix/plugins/paypal2/payment.py:1098 pretix/plugins/stripe/payment.py:1816
msgid "PayPal"
msgstr "ペイパル"
msgstr "PayPal"
#: pretix/plugins/paypal/apps.py:53
msgid ""
@@ -30233,7 +30217,7 @@ msgstr ""
#: pretix/plugins/paypal/payment.py:104
msgid "PayPal account"
msgstr "ペイパルアカウント"
msgstr "PayPalアカウント"
#: pretix/plugins/paypal/payment.py:117 pretix/plugins/paypal2/payment.py:114
#, python-brace-format
@@ -30330,7 +30314,7 @@ msgstr "PayPalの支払いID"
#: pretix/plugins/paypal/payment.py:711 pretix/plugins/paypal2/payment.py:1080
msgid "PayPal sale ID"
msgstr "ペイパルの売上ID"
msgstr "PayPalの売上ID"
#: pretix/plugins/paypal/templates/pretixplugins/paypal/checkout_payment_confirm.html:3
#: pretix/plugins/paypal2/templates/pretixplugins/paypal2/checkout_payment_confirm.html:19

View File

@@ -20,10 +20,12 @@
# <https://www.gnu.org/licenses/>.
#
import datetime
from collections import namedtuple
from urllib.parse import urlparse
import vobject
from django.conf import settings
from django.db.models import prefetch_related_objects
from django.utils.formats import date_format
from django.utils.translation import gettext as _
@@ -122,61 +124,109 @@ def get_private_icals(event, positions):
creation_time = datetime.datetime.now(datetime.timezone.utc)
calobjects = []
calentries = set() # using set for automatic deduplication of CalEntries
CalEntry = namedtuple('CalEntry', ['summary', 'description', 'location', 'dtstart', 'dtend', 'uid'])
evs = set(p.subevent or event for p in positions)
for ev in evs:
if isinstance(ev, Event):
# collecting the positions' calendar entries, preferring the most exact date and time available (positions > subevent > event)
prefetch_related_objects(positions, 'item__program_times')
for p in positions:
ev = p.subevent or event
program_times = p.item.program_times.all()
if program_times:
# if program times have been configured, they are preferred for the position's calendar entries
url = build_absolute_uri(event, 'presale:event.index')
for index, pt in enumerate(program_times):
summary = _('{event} - {item}').format(event=ev, item=p.item.name)
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
descr.append(str(_('Start: {datetime}')).format(
datetime=date_format(pt.start.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
descr.append(str(_('End: {datetime}')).format(
datetime=date_format(pt.end.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
location = None
dtstart = pt.start.astimezone(tz)
dtend = pt.end.astimezone(tz)
uid = 'pretix-{}-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
p.item.id,
index,
urlparse(url).netloc
)
calentries.add(CalEntry(summary, description, location, dtstart, dtend, uid))
else:
url = build_absolute_uri(event, 'presale:event.index', {
'subevent': ev.pk
})
# without program times, the subevent or event times are used for calendar entries, preferring subevents
if p.subevent:
url = build_absolute_uri(event, 'presale:event.index', {
'subevent': p.subevent.pk
})
else:
url = build_absolute_uri(event, 'presale:event.index')
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
if ev.date_admission:
descr.append(str(_('Admission: {datetime}')).format(
datetime=date_format(ev.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
if event.settings.mail_attach_ical_description:
ctx = get_email_context(event=event, event_or_subevent=ev)
description = format_map(str(event.settings.mail_attach_ical_description), ctx)
else:
# Default description
descr = []
descr.append(_('Tickets: {url}').format(url=url))
if ev.date_admission:
descr.append(str(_('Admission: {datetime}')).format(
datetime=date_format(ev.date_admission.astimezone(tz), 'SHORT_DATETIME_FORMAT')
))
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
# Actual ical organizer field is not useful since it will cause "your invitation was accepted" emails to the organizer
descr.append(_('Organizer: {organizer}').format(organizer=event.organizer.name))
description = '\n'.join(descr)
summary = str(ev.name)
if ev.location:
location = ", ".join(l.strip() for l in str(ev.location).splitlines() if l.strip())
else:
location = None
if event.settings.show_times:
dtstart = ev.date_from.astimezone(tz)
else:
dtstart = ev.date_from.astimezone(tz).date()
if event.settings.show_date_to and ev.date_to:
if event.settings.show_times:
dtend = ev.date_to.astimezone(tz)
else:
# with full-day events date_to in pretix is included (e.g. last day)
# whereas dtend in vcalendar is non-inclusive => add one day for export
dtend = ev.date_to.astimezone(tz).date() + datetime.timedelta(days=1)
else:
dtend = None
uid = 'pretix-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
ev.pk if p.subevent else '0',
urlparse(url).netloc
)
calentries.add(CalEntry(summary, description, location, dtstart, dtend, uid))
for calentry in calentries:
cal = vobject.iCalendar()
cal.add('prodid').value = '-//pretix//{}//'.format(settings.PRETIX_INSTANCE_NAME.replace(" ", "_"))
vevent = cal.add('vevent')
vevent.add('summary').value = str(ev.name)
vevent.add('description').value = description
vevent.add('summary').value = calentry.summary
vevent.add('description').value = calentry.description
vevent.add('dtstamp').value = creation_time
if ev.location:
vevent.add('location').value = ", ".join(l.strip() for l in str(ev.location).splitlines() if l.strip())
vevent.add('uid').value = 'pretix-{}-{}-{}@{}'.format(
event.organizer.slug,
event.slug,
ev.pk if not isinstance(ev, Event) else '0',
urlparse(url).netloc
)
if event.settings.show_times:
vevent.add('dtstart').value = ev.date_from.astimezone(tz)
else:
vevent.add('dtstart').value = ev.date_from.astimezone(tz).date()
if event.settings.show_date_to and ev.date_to:
if event.settings.show_times:
vevent.add('dtend').value = ev.date_to.astimezone(tz)
else:
# with full-day events date_to in pretix is included (e.g. last day)
# whereas dtend in vcalendar is non-inclusive => add one day for export
vevent.add('dtend').value = ev.date_to.astimezone(tz).date() + datetime.timedelta(days=1)
if calentry.location:
vevent.add('location').value = calentry.location
vevent.add('uid').value = calentry.uid
vevent.add('dtstart').value = calentry.dtstart
if calentry.dtend:
vevent.add('dtend').value = calentry.dtend
calobjects.append(cal)
return calobjects

View File

@@ -9,34 +9,6 @@
{% endblock %}
{% block inner %}
<h3 class="sr-only">{% trans "Payment" %}</h3>
{% if customer_gift_cards %}
<p><strong>
<span class="sr-only">{% trans "Information" %}</span>
{% trans "The following gift cards are available in your customer account:" %}
</strong></p>
<form method="post">
{% csrf_token %}
<ul class="list-group">
{% for c in customer_gift_cards %}
<li class="list-group-item row">
<div class="col-xs-9" id="gc-code-{{ forloop.counter }}">
{{ c }}
</div>
<div class="col-xs-2 text-right" id="gc-value-{{ forloop.counter }}">
{{ c.value|money:c.currency }}
</div>
<div class="col-xs-1 text-right">
<button name="use_giftcard" value="{{ c.secret }}" title="{% trans "Use gift card" %}"
aria-describedby="gc-code-{{ forloop.counter }} gc-value-{{ forloop.counter }}"
class="btn btn-primary btn-xs">
{% trans "Apply" %}
</button>
</div>
</li>
{% endfor %}
</ul>
</form>
{% endif %}
{% if current_payments %}
<p>{% trans "You already selected the following payment methods:" %}</p>
<form method="post">

View File

@@ -0,0 +1,33 @@
{% load i18n %}
{% load bootstrap3 %}
{% load money %}
{% load rich_text %}
{{ request.event.settings.payment_giftcard_public_description|rich_text }}
{% if customer_gift_cards %}
<p><strong>
<span class="sr-only">{% trans "Information" %}</span>
{% trans "The following gift cards are available in your customer account:" %}
</strong></p>
{% csrf_token %}
<ul class="list-group">
{% for c in customer_gift_cards %}
<li class="list-group-item row row-no-gutters">
<div class="col-sm-8 col-md-9" id="gc-code-{{ forloop.counter }}">
{{ c }}
</div>
<div class="col-sm-2 text-right" id="gc-value-{{ forloop.counter }}">
{{ c.value|money:c.currency }}
</div>
<div class="col-sm-2 col-md-1 text-right">
<button name="use_giftcard" class="btn btn-primary btn-xs use_giftcard" data-value="{{ c.secret }}"
title="{% trans "Use gift card" %}"
aria-describedby="gc-code-{{ forloop.counter }} gc-value-{{ forloop.counter }}">
{% trans "Apply" %}
</button>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
{% bootstrap_form form layout='checkout' %}

View File

@@ -8,10 +8,10 @@
"name": "pretix",
"version": "0.0.0",
"dependencies": {
"@babel/core": "^7.28.4",
"@babel/preset-env": "^7.28.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^16.0.1",
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"rollup": "^2.79.1",
"rollup-plugin-vue": "^5.0.1",
"vue": "^2.7.16",
@@ -33,27 +33,27 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/generator": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -89,12 +89,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"dependencies": {
"@babel/parser": "^7.28.3",
"@babel/types": "^7.28.2",
"@babel/parser": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -348,10 +348,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"engines": {
"node": ">=6.9.0"
}
@@ -392,11 +391,11 @@
}
},
"node_modules/@babel/parser": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dependencies": {
"@babel/types": "^7.28.4"
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -406,13 +405,12 @@
}
},
"node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz",
"integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==",
"license": "MIT",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.27.1"
"@babel/traverse": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -603,9 +601,9 @@
}
},
"node_modules/@babel/plugin-transform-block-scoping": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
"integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz",
"integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -648,16 +646,16 @@
}
},
"node_modules/@babel/plugin-transform-classes": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz",
"integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
"integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-globals": "^7.28.0",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1",
"@babel/traverse": "^7.28.3"
"@babel/traverse": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -683,12 +681,12 @@
}
},
"node_modules/@babel/plugin-transform-destructuring": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz",
"integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz",
"integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.28.0"
"@babel/traverse": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -775,10 +773,9 @@
}
},
"node_modules/@babel/plugin-transform-exponentiation-operator": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
"integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==",
"license": "MIT",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz",
"integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -868,10 +865,9 @@
}
},
"node_modules/@babel/plugin-transform-logical-assignment-operators": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz",
"integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==",
"license": "MIT",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz",
"integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -930,15 +926,14 @@
}
},
"node_modules/@babel/plugin-transform-modules-systemjs": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz",
"integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==",
"license": "MIT",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.27.1"
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1025,15 +1020,15 @@
}
},
"node_modules/@babel/plugin-transform-object-rest-spread": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
"integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz",
"integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==",
"dependencies": {
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-parameters": "^7.27.7",
"@babel/traverse": "^7.28.0"
"@babel/traverse": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
@@ -1074,10 +1069,9 @@
}
},
"node_modules/@babel/plugin-transform-optional-chaining": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
"integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
"license": "MIT",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz",
"integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
@@ -1152,9 +1146,9 @@
}
},
"node_modules/@babel/plugin-transform-regenerator": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz",
"integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz",
"integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1336,15 +1330,15 @@
}
},
"node_modules/@babel/preset-env": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz",
"integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz",
"integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==",
"dependencies": {
"@babel/compat-data": "^7.28.0",
"@babel/compat-data": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
@@ -1357,42 +1351,42 @@
"@babel/plugin-transform-async-generator-functions": "^7.28.0",
"@babel/plugin-transform-async-to-generator": "^7.27.1",
"@babel/plugin-transform-block-scoped-functions": "^7.27.1",
"@babel/plugin-transform-block-scoping": "^7.28.0",
"@babel/plugin-transform-block-scoping": "^7.28.5",
"@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-class-static-block": "^7.28.3",
"@babel/plugin-transform-classes": "^7.28.3",
"@babel/plugin-transform-classes": "^7.28.4",
"@babel/plugin-transform-computed-properties": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-destructuring": "^7.28.5",
"@babel/plugin-transform-dotall-regex": "^7.27.1",
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-dynamic-import": "^7.27.1",
"@babel/plugin-transform-explicit-resource-management": "^7.28.0",
"@babel/plugin-transform-exponentiation-operator": "^7.27.1",
"@babel/plugin-transform-exponentiation-operator": "^7.28.5",
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-for-of": "^7.27.1",
"@babel/plugin-transform-function-name": "^7.27.1",
"@babel/plugin-transform-json-strings": "^7.27.1",
"@babel/plugin-transform-literals": "^7.27.1",
"@babel/plugin-transform-logical-assignment-operators": "^7.27.1",
"@babel/plugin-transform-logical-assignment-operators": "^7.28.5",
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
"@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
"@babel/plugin-transform-modules-systemjs": "^7.27.1",
"@babel/plugin-transform-modules-systemjs": "^7.28.5",
"@babel/plugin-transform-modules-umd": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-new-target": "^7.27.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-numeric-separator": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
"@babel/plugin-transform-object-rest-spread": "^7.28.4",
"@babel/plugin-transform-object-super": "^7.27.1",
"@babel/plugin-transform-optional-catch-binding": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.28.5",
"@babel/plugin-transform-parameters": "^7.27.7",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@babel/plugin-transform-property-literals": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.28.3",
"@babel/plugin-transform-regenerator": "^7.28.4",
"@babel/plugin-transform-regexp-modifiers": "^7.27.1",
"@babel/plugin-transform-reserved-words": "^7.27.1",
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
@@ -1454,16 +1448,16 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/generator": "^7.28.5",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.4",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4",
"@babel/types": "^7.28.5",
"debug": "^4.3.1"
},
"engines": {
@@ -1471,12 +1465,12 @@
}
},
"node_modules/@babel/types": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1523,9 +1517,9 @@
}
},
"node_modules/@rollup/plugin-babel": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz",
"integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz",
"integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==",
"dependencies": {
"@babel/helper-module-imports": "^7.18.6",
"@rollup/pluginutils": "^5.0.1"
@@ -1548,10 +1542,9 @@
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
"license": "MIT",
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
@@ -3790,24 +3783,24 @@
}
},
"@babel/compat-data": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz",
"integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="
},
"@babel/core": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"requires": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/generator": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -3829,12 +3822,12 @@
}
},
"@babel/generator": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"requires": {
"@babel/parser": "^7.28.3",
"@babel/types": "^7.28.2",
"@babel/parser": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -4011,9 +4004,9 @@
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
},
"@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="
},
"@babel/helper-validator-option": {
"version": "7.27.1",
@@ -4040,20 +4033,20 @@
}
},
"@babel/parser": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"requires": {
"@babel/types": "^7.28.4"
"@babel/types": "^7.28.5"
}
},
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz",
"integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
"requires": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.27.1"
"@babel/traverse": "^7.28.5"
}
},
"@babel/plugin-bugfix-safari-class-field-initializer-scope": {
@@ -4159,9 +4152,9 @@
}
},
"@babel/plugin-transform-block-scoping": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz",
"integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz",
"integrity": "sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g==",
"requires": {
"@babel/helper-plugin-utils": "^7.27.1"
}
@@ -4185,16 +4178,16 @@
}
},
"@babel/plugin-transform-classes": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.3.tgz",
"integrity": "sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz",
"integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==",
"requires": {
"@babel/helper-annotate-as-pure": "^7.27.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-globals": "^7.28.0",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1",
"@babel/traverse": "^7.28.3"
"@babel/traverse": "^7.28.4"
}
},
"@babel/plugin-transform-computed-properties": {
@@ -4207,12 +4200,12 @@
}
},
"@babel/plugin-transform-destructuring": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz",
"integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz",
"integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==",
"requires": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.28.0"
"@babel/traverse": "^7.28.5"
}
},
"@babel/plugin-transform-dotall-regex": {
@@ -4259,9 +4252,9 @@
}
},
"@babel/plugin-transform-exponentiation-operator": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz",
"integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz",
"integrity": "sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw==",
"requires": {
"@babel/helper-plugin-utils": "^7.27.1"
}
@@ -4310,9 +4303,9 @@
}
},
"@babel/plugin-transform-logical-assignment-operators": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz",
"integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz",
"integrity": "sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA==",
"requires": {
"@babel/helper-plugin-utils": "^7.27.1"
}
@@ -4344,14 +4337,14 @@
}
},
"@babel/plugin-transform-modules-systemjs": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz",
"integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
"requires": {
"@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.27.1"
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.5"
}
},
"@babel/plugin-transform-modules-umd": {
@@ -4397,15 +4390,15 @@
}
},
"@babel/plugin-transform-object-rest-spread": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz",
"integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz",
"integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==",
"requires": {
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-parameters": "^7.27.7",
"@babel/traverse": "^7.28.0"
"@babel/traverse": "^7.28.4"
}
},
"@babel/plugin-transform-object-super": {
@@ -4426,9 +4419,9 @@
}
},
"@babel/plugin-transform-optional-chaining": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz",
"integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz",
"integrity": "sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ==",
"requires": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1"
@@ -4470,9 +4463,9 @@
}
},
"@babel/plugin-transform-regenerator": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.3.tgz",
"integrity": "sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz",
"integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==",
"requires": {
"@babel/helper-plugin-utils": "^7.27.1"
}
@@ -4571,15 +4564,15 @@
}
},
"@babel/preset-env": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz",
"integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz",
"integrity": "sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg==",
"requires": {
"@babel/compat-data": "^7.28.0",
"@babel/compat-data": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-validator-option": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1",
"@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5",
"@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1",
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1",
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1",
@@ -4592,42 +4585,42 @@
"@babel/plugin-transform-async-generator-functions": "^7.28.0",
"@babel/plugin-transform-async-to-generator": "^7.27.1",
"@babel/plugin-transform-block-scoped-functions": "^7.27.1",
"@babel/plugin-transform-block-scoping": "^7.28.0",
"@babel/plugin-transform-block-scoping": "^7.28.5",
"@babel/plugin-transform-class-properties": "^7.27.1",
"@babel/plugin-transform-class-static-block": "^7.28.3",
"@babel/plugin-transform-classes": "^7.28.3",
"@babel/plugin-transform-classes": "^7.28.4",
"@babel/plugin-transform-computed-properties": "^7.27.1",
"@babel/plugin-transform-destructuring": "^7.28.0",
"@babel/plugin-transform-destructuring": "^7.28.5",
"@babel/plugin-transform-dotall-regex": "^7.27.1",
"@babel/plugin-transform-duplicate-keys": "^7.27.1",
"@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-dynamic-import": "^7.27.1",
"@babel/plugin-transform-explicit-resource-management": "^7.28.0",
"@babel/plugin-transform-exponentiation-operator": "^7.27.1",
"@babel/plugin-transform-exponentiation-operator": "^7.28.5",
"@babel/plugin-transform-export-namespace-from": "^7.27.1",
"@babel/plugin-transform-for-of": "^7.27.1",
"@babel/plugin-transform-function-name": "^7.27.1",
"@babel/plugin-transform-json-strings": "^7.27.1",
"@babel/plugin-transform-literals": "^7.27.1",
"@babel/plugin-transform-logical-assignment-operators": "^7.27.1",
"@babel/plugin-transform-logical-assignment-operators": "^7.28.5",
"@babel/plugin-transform-member-expression-literals": "^7.27.1",
"@babel/plugin-transform-modules-amd": "^7.27.1",
"@babel/plugin-transform-modules-commonjs": "^7.27.1",
"@babel/plugin-transform-modules-systemjs": "^7.27.1",
"@babel/plugin-transform-modules-systemjs": "^7.28.5",
"@babel/plugin-transform-modules-umd": "^7.27.1",
"@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1",
"@babel/plugin-transform-new-target": "^7.27.1",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
"@babel/plugin-transform-numeric-separator": "^7.27.1",
"@babel/plugin-transform-object-rest-spread": "^7.28.0",
"@babel/plugin-transform-object-rest-spread": "^7.28.4",
"@babel/plugin-transform-object-super": "^7.27.1",
"@babel/plugin-transform-optional-catch-binding": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.27.1",
"@babel/plugin-transform-optional-chaining": "^7.28.5",
"@babel/plugin-transform-parameters": "^7.27.7",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/plugin-transform-private-property-in-object": "^7.27.1",
"@babel/plugin-transform-property-literals": "^7.27.1",
"@babel/plugin-transform-regenerator": "^7.28.3",
"@babel/plugin-transform-regenerator": "^7.28.4",
"@babel/plugin-transform-regexp-modifiers": "^7.27.1",
"@babel/plugin-transform-reserved-words": "^7.27.1",
"@babel/plugin-transform-shorthand-properties": "^7.27.1",
@@ -4675,26 +4668,26 @@
}
},
"@babel/traverse": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"requires": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/generator": "^7.28.5",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.4",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4",
"@babel/types": "^7.28.5",
"debug": "^4.3.1"
}
},
"@babel/types": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"requires": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
"@babel/helper-validator-identifier": "^7.28.5"
}
},
"@jridgewell/gen-mapping": {
@@ -4735,18 +4728,18 @@
}
},
"@rollup/plugin-babel": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz",
"integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz",
"integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==",
"requires": {
"@babel/helper-module-imports": "^7.18.6",
"@rollup/pluginutils": "^5.0.1"
}
},
"@rollup/plugin-node-resolve": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz",
"integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==",
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz",
"integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==",
"requires": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",

View File

@@ -4,10 +4,10 @@
"private": true,
"scripts": {},
"dependencies": {
"@babel/core": "^7.28.4",
"@babel/preset-env": "^7.28.3",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-node-resolve": "^16.0.1",
"@babel/core": "^7.28.5",
"@babel/preset-env": "^7.28.5",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"vue": "^2.7.16",
"rollup": "^2.79.1",
"rollup-plugin-vue": "^5.0.1",

View File

@@ -263,3 +263,11 @@ svg.svg-icon {
@include table-row-variant('warning', var(--pretix-brand-warning-lighten-40), var(--pretix-brand-warning-lighten-35));
@include table-row-variant('danger', var(--pretix-brand-danger-lighten-30), var(--pretix-brand-danger-lighten-25));
.confirmation-code-input {
font-size: 200%;
font-family: monospace;
font-stretch: expanded;
text-align: center;
height: 50px;
margin: 10px 0;
}

View File

@@ -938,3 +938,25 @@ details {
}
}
}
@media (min-width: $screen-lg-min) {
.centered-form {
margin: 80px auto;
max-width: 800px;
border: 1px solid #ddd;
padding: 20px 40px 0;
border-radius: 4px;
box-shadow: 2px 2px 2px #eee;
}
.form.centered-form .submit-group {
margin: 25px -40px 0 !important;
padding-right: 40px;
padding-left: 40px;
}
.centered-form p {
margin: 20px 0;
}
}

View File

@@ -46,7 +46,8 @@ from tests.const import SAMPLE_PNG
from pretix.base.models import (
CartPosition, InvoiceAddress, Item, ItemAddOn, ItemBundle, ItemCategory,
ItemVariation, Order, OrderPosition, Question, QuestionOption, Quota,
ItemProgramTime, ItemVariation, Order, OrderPosition, Question,
QuestionOption, Quota,
)
from pretix.base.models.orders import OrderFee
@@ -331,6 +332,7 @@ TEST_ITEM_RES = {
"variations": [],
"addons": [],
"bundles": [],
"program_times": [],
"show_quota_left": None,
"original_price": None,
"free_price_suggestion": None,
@@ -509,6 +511,24 @@ def test_item_detail_bundles(token_client, organizer, event, team, item, categor
assert res == resp.data
@pytest.mark.django_db
def test_item_detail_program_times(token_client, organizer, event, team, item, category):
with scopes_disabled():
item.program_times.create(item=item, start=datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc))
res = dict(TEST_ITEM_RES)
res["id"] = item.pk
res["program_times"] = [{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
}]
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res == resp.data
@pytest.mark.django_db
def test_item_create(token_client, organizer, event, item, category, taxrule, membership_type):
resp = token_client.post(
@@ -1072,6 +1092,57 @@ def test_item_create_with_bundle(token_client, organizer, event, item, category,
assert resp.content.decode() == '{"bundles":["The chosen variation does not belong to this item."]}'
@pytest.mark.django_db
def test_item_create_with_product_time(token_client, organizer, event, item, category, taxrule):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/'.format(organizer.slug, event.slug),
{
"category": category.pk,
"name": {
"en": "Ticket"
},
"active": True,
"description": None,
"default_price": "23.00",
"free_price": False,
"tax_rate": "19.00",
"tax_rule": taxrule.pk,
"admission": True,
"issue_giftcard": False,
"position": 0,
"picture": None,
"available_from": None,
"available_until": None,
"require_voucher": False,
"hide_without_voucher": False,
"allow_cancel": True,
"min_per_order": None,
"max_per_order": None,
"checkin_attention": False,
"checkin_text": None,
"has_variations": False,
"program_times": [
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
},
{
"start": "2017-12-29T00:00:00Z",
"end": "2017-12-30T00:00:00Z",
}
]
},
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
new_item = Item.objects.get(pk=resp.data['id'])
assert new_item.program_times.first().start == datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc)
assert new_item.program_times.first().end == datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc)
assert new_item.program_times.last().start == datetime(2017, 12, 29, 0, 0, 0, tzinfo=timezone.utc)
assert new_item.program_times.last().end == datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc)
@pytest.mark.django_db(transaction=True)
def test_item_update(token_client, organizer, event, item, category, item2, category2, taxrule2):
resp = token_client.patch(
@@ -1147,8 +1218,8 @@ def test_item_update(token_client, organizer, event, item, category, item2, cate
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/'.format(organizer.slug, event.slug, item.pk),
@@ -1165,8 +1236,8 @@ def test_item_update(token_client, organizer, event, item, category, item2, cate
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
item.personalized = True
item.admission = True
@@ -1322,8 +1393,8 @@ def test_item_update_with_variation(token_client, organizer, event, item):
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
@pytest.mark.django_db
@@ -1345,8 +1416,8 @@ def test_item_update_with_addon(token_client, organizer, event, item, category):
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, or variations via PATCH/PUT is not supported. Please use ' \
'the dedicated nested endpoint."]}'
assert resp.content.decode() == '{"non_field_errors":["Updating add-ons, bundles, program times or variations via ' \
'PATCH/PUT is not supported. Please use the dedicated nested endpoint."]}'
@pytest.mark.django_db
@@ -1881,6 +1952,123 @@ def test_addons_delete(token_client, organizer, event, item, addon):
assert not item.addons.filter(pk=addon.id).exists()
@pytest.fixture
def program_time(item, category):
return item.program_times.create(start=datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc))
@pytest.fixture
def program_time2(item, category):
return item.program_times.create(start=datetime(2017, 12, 29, 0, 0, 0, tzinfo=timezone.utc),
end=datetime(2017, 12, 30, 0, 0, 0, tzinfo=timezone.utc))
TEST_PROGRAM_TIMES_RES = {
0: {
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z",
},
1: {
"start": "2017-12-29T00:00:00Z",
"end": "2017-12-30T00:00:00Z",
}
}
@pytest.mark.django_db
def test_program_times_list(token_client, organizer, event, item, program_time, program_time2):
res = dict(TEST_PROGRAM_TIMES_RES)
res[0]["id"] = program_time.pk
res[1]["id"] = program_time2.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug,
item.pk))
assert resp.status_code == 200
assert res[0]['start'] == resp.data['results'][0]['start']
assert res[0]['end'] == resp.data['results'][0]['end']
assert res[0]['id'] == resp.data['results'][0]['id']
assert res[1]['start'] == resp.data['results'][1]['start']
assert res[1]['end'] == resp.data['results'][1]['end']
assert res[1]['id'] == resp.data['results'][1]['id']
@pytest.mark.django_db
def test_program_times_detail(token_client, organizer, event, item, program_time):
res = dict(TEST_PROGRAM_TIMES_RES)
res[0]["id"] = program_time.pk
resp = token_client.get(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug,
item.pk, program_time.pk))
assert resp.status_code == 200
assert res[0] == resp.data
@pytest.mark.django_db
def test_program_times_create(token_client, organizer, event, item):
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-27T00:00:00Z",
"end": "2017-12-28T00:00:00Z"
},
format='json'
)
assert resp.status_code == 201
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert datetime(2017, 12, 27, 0, 0, 0, tzinfo=timezone.utc) == program_time.start
assert datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc) == program_time.end
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/'.format(organizer.slug, event.slug, item.pk),
{
"start": "2017-12-28T00:00:00Z",
"end": "2017-12-27T00:00:00Z"
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The program end must not be before the program start."]}'
@pytest.mark.django_db
def test_program_times_update(token_client, organizer, event, item, program_time):
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug, item.pk,
program_time.pk),
{
"start": "2017-12-26T00:00:00Z"
},
format='json'
)
assert resp.status_code == 200
with scopes_disabled():
program_time = ItemProgramTime.objects.get(pk=resp.data['id'])
assert datetime(2017, 12, 26, 0, 0, 0, tzinfo=timezone.utc) == program_time.start
assert datetime(2017, 12, 28, 0, 0, 0, tzinfo=timezone.utc) == program_time.end
resp = token_client.patch(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug, item.pk,
program_time.pk),
{
"start": "2017-12-30T00:00:00Z"
},
format='json'
)
assert resp.status_code == 400
assert resp.content.decode() == '{"non_field_errors":["The program end must not be before the program start."]}'
@pytest.mark.django_db
def test_program_times_delete(token_client, organizer, event, item, program_time):
resp = token_client.delete(
'/api/v1/organizers/{}/events/{}/items/{}/program_times/{}/'.format(organizer.slug, event.slug,
item.pk, program_time.pk))
assert resp.status_code == 204
with scopes_disabled():
assert not item.program_times.filter(pk=program_time.id).exists()
@pytest.fixture
def quota(event, item):
q = event.quotas.create(name="Budget Quota", size=200)

View File

@@ -1996,7 +1996,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['positions'][0].get('pdf_data')
# order list
with django_assert_max_num_queries(32):
with django_assert_max_num_queries(33):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
))

View File

@@ -39,7 +39,9 @@ import pytest
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import Event, Organizer, Question, SeatingPlan
from pretix.base.models import (
Event, ItemProgramTime, Organizer, Question, SeatingPlan,
)
from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
@@ -78,6 +80,10 @@ def test_full_clone_same_organizer():
# todo: test that item pictures are copied, not linked
ItemMetaValue.objects.create(item=item1, property=item_meta, value="Foo")
assert item1.meta_data
ItemProgramTime.objects.create(item=item1,
start=datetime.datetime(2017, 12, 27, 0, 0, 0, tzinfo=datetime.timezone.utc),
end=datetime.datetime(2017, 12, 28, 0, 0, 0, tzinfo=datetime.timezone.utc))
assert item1.program_times
item2 = event.items.create(category=category, tax_rule=tax_rule, name="T-shirt", default_price=15,
hidden_if_item_available=item1)
item2v = item2.variations.create(value="red", default_price=15, all_sales_channels=False)
@@ -161,6 +167,8 @@ def test_full_clone_same_organizer():
assert copied_item1.category == copied_event.categories.get(name='Tickets')
assert copied_item1.limit_sales_channels.get() == sc
assert copied_item1.meta_data == item1.meta_data
assert copied_item1.program_times.first().start == item1.program_times.first().start
assert copied_item1.program_times.first().end == item1.program_times.first().end
assert copied_item2.variations.get().meta_data == item2v.meta_data
assert copied_item1.hidden_if_available == copied_q2
assert copied_item1.grant_membership_type == membership_type

View File

@@ -1134,7 +1134,7 @@ class PasswordChangeRequiredTest(TestCase):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
def test_redirect_to_settings(self):
def test_redirect_to_password_change(self):
self.user.needs_password_change = True
self.user.save()
self.client.login(email='dummy@dummy.dummy', password='dummy')
@@ -1143,9 +1143,9 @@ class PasswordChangeRequiredTest(TestCase):
self.assertEqual(response.status_code, 302)
assert self.user.needs_password_change is True
self.assertIn('/control/settings?next=/control/events/', response['Location'])
self.assertIn('/control/settings/password/change?next=/control/events/', response['Location'])
def test_redirect_to_2fa_to_settings(self):
def test_redirect_to_2fa_to_password_change(self):
self.user.require_2fa = True
self.user.needs_password_change = True
self.user.save()
@@ -1168,4 +1168,4 @@ class PasswordChangeRequiredTest(TestCase):
response = self.client.get('/control/events/')
self.assertEqual(response.status_code, 302)
self.assertIn('/control/settings?next=/control/events/', response['Location'])
self.assertIn('/control/settings/password/change?next=/control/events/', response['Location'])

View File

@@ -681,6 +681,10 @@ class ItemsTest(ItemFormTest):
self.var2.save()
prop = self.event1.item_meta_properties.create(name="Foo")
self.item2.meta_values.create(property=prop, value="Bar")
self.item2.program_times.create(start=datetime.datetime(2017, 12, 27, 0, 0, 0,
tzinfo=datetime.timezone.utc),
end=datetime.datetime(2017, 12, 28, 0, 0, 0,
tzinfo=datetime.timezone.utc))
doc = self.get_doc('/control/event/%s/%s/items/add?copy_from=%d' % (self.orga1.slug, self.event1.slug, self.item2.pk))
data = extract_form_fields(doc.select("form")[0])
@@ -709,6 +713,8 @@ class ItemsTest(ItemFormTest):
assert i_new.meta_data == i_old.meta_data == {"Foo": "Bar"}
assert set(i_new.questions.all()) == set(i_old.questions.all())
assert set([str(v.value) for v in i_new.variations.all()]) == set([str(v.value) for v in i_old.variations.all()])
assert i_old.program_times.first().start == i_new.program_times.first().start
assert i_old.program_times.first().end == i_new.program_times.first().end
def test_add_to_existing_quota(self):
with scopes_disabled():

View File

@@ -19,22 +19,11 @@
# 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: Jason Estibeiro
#
# 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 re
import time
import pytest
from django.core import mail as djmail
from django.utils.timezone import now
from django_otp.oath import TOTP
from django_otp.plugins.otp_static.models import StaticDevice
@@ -56,7 +45,7 @@ class UserSettingsTest(SoupTest):
self.user = User.objects.create_user('dummy@dummy.dummy', 'barfoofoo')
self.client.login(email='dummy@dummy.dummy', password='barfoofoo')
doc = self.get_doc('/control/settings')
self.form_data = extract_form_fields(doc.select('.container-fluid form')[0])
self.form_data = extract_form_fields(doc.select('form[data-testid="usersettingsform"]')[0])
def save(self, data):
form_data = self.form_data.copy()
@@ -71,33 +60,107 @@ class UserSettingsTest(SoupTest):
self.user = User.objects.get(pk=self.user.pk)
assert self.user.fullname == 'Peter Miller'
def test_change_email_require_password(self):
def test_set_locale_and_timezone(self):
doc = self.save({
'email': 'foo@example.com',
'locale': 'fr',
'timezone': 'Europe/Paris',
})
assert doc.select(".alert-danger")
assert doc.select(".alert-success")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.email == 'dummy@dummy.dummy'
assert self.user.locale == 'fr'
assert self.user.timezone == 'Europe/Paris'
class UserEmailChangeTest(SoupTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'barfoofoo')
self.client.login(email='dummy@dummy.dummy', password='barfoofoo')
session = self.client.session
session['pretix_auth_login_time'] = int(time.time())
session.save()
doc = self.get_doc('/control/settings/email/change')
self.form_data = extract_form_fields(doc.select('.container-fluid form')[0])
def test_require_reauth(self):
session = self.client.session
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 2
session.save()
response = self.client.get('/control/settings/email/change')
self.assertIn('/control/reauth', response['Location'])
self.assertEqual(response.status_code, 302)
response = self.client.post('/control/reauth/?next=/control/settings/email/change', {
'password': 'barfoofoo'
})
self.assertIn('/control/settings/email/change', response['Location'])
self.assertEqual(response.status_code, 302)
def submit_step_1(self, data):
form_data = self.form_data.copy()
form_data.update(data)
return self.post_doc('/control/settings/email/change', form_data)
def submit_step_2(self, data):
form_data = self.form_data.copy()
form_data.update(data)
return self.post_doc('/control/settings/email/confirm?reason=email_change', form_data)
def test_change_email_success(self):
doc = self.save({
'email': 'foo@example.com',
'old_pw': 'barfoofoo'
djmail.outbox = []
doc = self.submit_step_1({
'new_email': 'foo@example.com',
})
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['foo@example.com']
code = re.search("[0-9]{7}", djmail.outbox[0].body).group(0)
doc = self.submit_step_2({
'code': code,
})
assert doc.select(".alert-success")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.email == 'foo@example.com'
def test_change_email_no_duplicates(self):
User.objects.create_user('foo@example.com', 'foo')
doc = self.save({
'email': 'foo@example.com',
'old_pw': 'barfoofoo'
def test_change_email_wrong_code(self):
djmail.outbox = []
doc = self.submit_step_1({
'new_email': 'foo@example.com',
})
assert len(djmail.outbox) == 1
assert djmail.outbox[0].to == ['foo@example.com']
code = re.search("[0-9]{7}", djmail.outbox[0].body).group(0)
wrong_code = '0000000' if code == '1234567' else '1234567'
doc = self.submit_step_2({
'code': wrong_code,
})
assert doc.select(".alert-danger")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.email == 'dummy@dummy.dummy'
def test_change_email_no_duplicates(self):
User.objects.create_user('foo@example.com', 'foo')
doc = self.submit_step_1({
'new_email': 'foo@example.com',
})
assert doc.select(".alert-danger")
self.user = User.objects.get(pk=self.user.pk)
assert self.user.email == 'dummy@dummy.dummy'
class UserPasswordChangeTest(SoupTest):
def setUp(self):
super().setUp()
self.user = User.objects.create_user('dummy@dummy.dummy', 'barfoofoo')
self.client.login(email='dummy@dummy.dummy', password='barfoofoo')
doc = self.get_doc('/control/settings/password/change')
self.form_data = extract_form_fields(doc.select('.container-fluid form')[0])
def save(self, data):
form_data = self.form_data.copy()
form_data.update(data)
return self.post_doc('/control/settings/password/change', form_data)
def test_change_password_require_password(self):
doc = self.save({
'new_pw': 'foo',
@@ -193,18 +256,6 @@ class UserSettingsTest(SoupTest):
})
assert doc.select(".alert-danger")
def test_needs_password_change(self):
self.user.needs_password_change = True
self.user.save()
doc = self.save({
'email': 'foo@example.com',
'old_pw': 'barfoofoo'
})
assert doc.select(".alert-success")
assert doc.select(".alert-warning")
self.user.refresh_from_db()
assert self.user.needs_password_change is True
def test_needs_password_change_changed(self):
self.user.needs_password_change = True
self.user.save()