mirror of
https://github.com/pretix/pretix.git
synced 2025-12-12 04:42:28 +00:00
Compare commits
1 Commits
context-on
...
api-taxrul
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fd683b5c8 |
@@ -20,11 +20,16 @@ internal_name string An optional nam
|
|||||||
rate decimal (string) Tax rate in percent
|
rate decimal (string) Tax rate in percent
|
||||||
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
price_includes_tax boolean If ``true`` (default), tax is assumed to be included in
|
||||||
the specified product price
|
the specified product price
|
||||||
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied
|
eu_reverse_charge boolean If ``true``, EU reverse charge rules are applied. Will
|
||||||
|
be ignored if custom rules are set.
|
||||||
home_country string Merchant country (required for reverse charge), can be
|
home_country string Merchant country (required for reverse charge), can be
|
||||||
``null`` or empty string
|
``null`` or empty string
|
||||||
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
keep_gross_if_rate_changes boolean If ``true``, changes of the tax rate based on custom
|
||||||
rules keep the gross price constant (default is ``false``)
|
rules keep the gross price constant (default is ``false``)
|
||||||
|
custom_rules object Dynamic rules specification. Each list element
|
||||||
|
corresponds to one rule that will be processed in order.
|
||||||
|
The current version of the schema in use can be found
|
||||||
|
`here`_.
|
||||||
===================================== ========================== =======================================================
|
===================================== ========================== =======================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -32,6 +37,10 @@ keep_gross_if_rate_changes boolean If ``true``, ch
|
|||||||
|
|
||||||
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
|
The ``internal_name`` and ``keep_gross_if_rate_changes`` attributes have been added.
|
||||||
|
|
||||||
|
.. versionchanged:: 2023.6
|
||||||
|
|
||||||
|
The ``custom_rules`` attribute has been added.
|
||||||
|
|
||||||
Endpoints
|
Endpoints
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@@ -68,6 +77,7 @@ Endpoints
|
|||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
"keep_gross_if_rate_changes": false,
|
"keep_gross_if_rate_changes": false,
|
||||||
|
"custom_rules": null,
|
||||||
"home_country": "DE"
|
"home_country": "DE"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -108,6 +118,7 @@ Endpoints
|
|||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
"keep_gross_if_rate_changes": false,
|
"keep_gross_if_rate_changes": false,
|
||||||
|
"custom_rules": null,
|
||||||
"home_country": "DE"
|
"home_country": "DE"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +167,7 @@ Endpoints
|
|||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
"keep_gross_if_rate_changes": false,
|
"keep_gross_if_rate_changes": false,
|
||||||
|
"custom_rules": null,
|
||||||
"home_country": "DE"
|
"home_country": "DE"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +215,7 @@ Endpoints
|
|||||||
"price_includes_tax": true,
|
"price_includes_tax": true,
|
||||||
"eu_reverse_charge": false,
|
"eu_reverse_charge": false,
|
||||||
"keep_gross_if_rate_changes": false,
|
"keep_gross_if_rate_changes": false,
|
||||||
|
"custom_rules": null,
|
||||||
"home_country": "DE"
|
"home_country": "DE"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,3 +255,5 @@ Endpoints
|
|||||||
:statuscode 204: no error
|
:statuscode 204: no error
|
||||||
:statuscode 401: Authentication failure
|
:statuscode 401: Authentication failure
|
||||||
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
:statuscode 403: The requested organizer/event/rule does not exist **or** you have no permission to change it **or** this tax rule cannot be deleted since it is currently in use.
|
||||||
|
|
||||||
|
.. _here: https://github.com/pretix/pretix/blob/master/src/pretix/static/schema/tax-rules-custom.schema.json
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import json
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
@@ -46,3 +48,16 @@ class AsymmetricField(serializers.Field):
|
|||||||
|
|
||||||
def run_validation(self, data=serializers.empty):
|
def run_validation(self, data=serializers.empty):
|
||||||
return self.write.run_validation(data)
|
return self.write.run_validation(data)
|
||||||
|
|
||||||
|
|
||||||
|
class CompatibleJSONField(serializers.JSONField):
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
try:
|
||||||
|
return json.dumps(data)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.fail('invalid')
|
||||||
|
|
||||||
|
def to_representation(self, value):
|
||||||
|
if value:
|
||||||
|
return json.loads(value)
|
||||||
|
return value
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ from rest_framework import serializers
|
|||||||
from rest_framework.fields import ChoiceField, Field
|
from rest_framework.fields import ChoiceField, Field
|
||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
|
|
||||||
|
from pretix.api.serializers import CompatibleJSONField
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.settings import SettingsSerializer
|
from pretix.api.serializers.settings import SettingsSerializer
|
||||||
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
from pretix.base.models import Device, Event, TaxRule, TeamAPIToken
|
||||||
@@ -53,6 +54,7 @@ from pretix.base.models.event import SubEvent
|
|||||||
from pretix.base.models.items import (
|
from pretix.base.models.items import (
|
||||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||||
)
|
)
|
||||||
|
from pretix.base.models.tax import CustomRulesValidator
|
||||||
from pretix.base.services.seating import (
|
from pretix.base.services.seating import (
|
||||||
SeatProtected, generate_seats, validate_plan_change,
|
SeatProtected, generate_seats, validate_plan_change,
|
||||||
)
|
)
|
||||||
@@ -650,9 +652,16 @@ class SubEventSerializer(I18nAwareModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
|
||||||
|
custom_rules = CompatibleJSONField(
|
||||||
|
validators=[CustomRulesValidator()],
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TaxRule
|
model = TaxRule
|
||||||
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name', 'keep_gross_if_rate_changes')
|
fields = ('id', 'name', 'rate', 'price_includes_tax', 'eu_reverse_charge', 'home_country', 'internal_name',
|
||||||
|
'keep_gross_if_rate_changes', 'custom_rules')
|
||||||
|
|
||||||
|
|
||||||
class EventSettingsSerializer(SettingsSerializer):
|
class EventSettingsSerializer(SettingsSerializer):
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from collections import Counter, defaultdict
|
from collections import Counter, defaultdict
|
||||||
@@ -39,6 +38,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_framework.relations import SlugRelatedField
|
from rest_framework.relations import SlugRelatedField
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
from pretix.api.serializers import CompatibleJSONField
|
||||||
from pretix.api.serializers.event import SubEventSerializer
|
from pretix.api.serializers.event import SubEventSerializer
|
||||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||||
from pretix.api.serializers.item import (
|
from pretix.api.serializers.item import (
|
||||||
@@ -896,19 +896,6 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class CompatibleJSONField(serializers.JSONField):
|
|
||||||
def to_internal_value(self, data):
|
|
||||||
try:
|
|
||||||
return json.dumps(data)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self.fail('invalid')
|
|
||||||
|
|
||||||
def to_representation(self, value):
|
|
||||||
if value:
|
|
||||||
return json.loads(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class WrappedList:
|
class WrappedList:
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|||||||
@@ -22,9 +22,12 @@
|
|||||||
import json
|
import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
|
from django.contrib.staticfiles import finders
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.deconstruct import deconstructible
|
||||||
from django.utils.formats import localize
|
from django.utils.formats import localize
|
||||||
from django.utils.translation import gettext_lazy as _, pgettext
|
from django.utils.translation import gettext_lazy as _, pgettext
|
||||||
from i18nfield.fields import I18nCharField
|
from i18nfield.fields import I18nCharField
|
||||||
@@ -135,6 +138,25 @@ def cc_to_vat_prefix(country_code):
|
|||||||
return country_code
|
return country_code
|
||||||
|
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class CustomRulesValidator:
|
||||||
|
def __call__(self, value):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
try:
|
||||||
|
val = json.loads(value)
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError(_('Your layout file is not a valid JSON file.'))
|
||||||
|
else:
|
||||||
|
val = value
|
||||||
|
with open(finders.find('schema/tax-rules-custom.schema.json'), 'r') as f:
|
||||||
|
schema = json.loads(f.read())
|
||||||
|
try:
|
||||||
|
jsonschema.validate(val, schema)
|
||||||
|
except jsonschema.ValidationError as e:
|
||||||
|
e = str(e).replace('%', '%%')
|
||||||
|
raise ValidationError(_('Your set of rules is not valid. Error message: {}').format(e))
|
||||||
|
|
||||||
|
|
||||||
class TaxRule(LoggedModel):
|
class TaxRule(LoggedModel):
|
||||||
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
|
event = models.ForeignKey('Event', related_name='tax_rules', on_delete=models.CASCADE)
|
||||||
internal_name = models.CharField(
|
internal_name = models.CharField(
|
||||||
|
|||||||
49
src/pretix/static/schema/tax-rules-custom.schema.json
Normal file
49
src/pretix/static/schema/tax-rules-custom.schema.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"title": "Tax rules",
|
||||||
|
"description": "Dynamic taxation rules",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "List of rules, executed in order until one matches",
|
||||||
|
"properties": {
|
||||||
|
"country": {
|
||||||
|
"description": "Country code to match. ZZ = any country, EU = any EU country.",
|
||||||
|
"enum": ["AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "EU", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW", "ZZ"]
|
||||||
|
},
|
||||||
|
"address_type": {
|
||||||
|
"description": "Type of customer, emtpy = any.",
|
||||||
|
"enum": ["", "individual", "business", "business_vat_id"]
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"description": "Action to take.",
|
||||||
|
"enum": ["vat", "reverse", "no", "block", "require_approval"]
|
||||||
|
},
|
||||||
|
"rate": {
|
||||||
|
"description": "Tax rate in case of action=vat or action=require_approval (or null for default)",
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"pattern": "^[0-9]+(\\.[0-9]+)?$"
|
||||||
|
},
|
||||||
|
"invoice_text": {
|
||||||
|
"description": "Text on invoice (localized)",
|
||||||
|
"type": ["object", "null"],
|
||||||
|
"patternProperties": {
|
||||||
|
"[a-zA-Z-]+": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"ORDER": {
|
||||||
|
"description": "Internal, for backwards-compatibility, will be ignored.",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"description": "Internal, for backwards-compatibility, will be ignored.",
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["country", "address_type", "action"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,8 @@ TEST_TAXRULE_RES = {
|
|||||||
'rate': '19.00',
|
'rate': '19.00',
|
||||||
'price_includes_tax': True,
|
'price_includes_tax': True,
|
||||||
'eu_reverse_charge': False,
|
'eu_reverse_charge': False,
|
||||||
'home_country': ''
|
'home_country': '',
|
||||||
|
'custom_rules': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +85,10 @@ def test_rule_update(token_client, organizer, event, taxrule):
|
|||||||
'/api/v1/organizers/{}/events/{}/taxrules/{}/'.format(organizer.slug, event.slug, taxrule.pk),
|
'/api/v1/organizers/{}/events/{}/taxrules/{}/'.format(organizer.slug, event.slug, taxrule.pk),
|
||||||
{
|
{
|
||||||
"rate": "20.00",
|
"rate": "20.00",
|
||||||
|
"custom_rules": [
|
||||||
|
{"country": "AT", "address_type": "", "action": "vat", "rate": "19.00",
|
||||||
|
"invoice_text": {"en": "Austrian VAT applies"}}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
format='json'
|
format='json'
|
||||||
)
|
)
|
||||||
@@ -111,3 +116,20 @@ def test_rule_delete_forbidden(token_client, organizer, event, taxrule):
|
|||||||
)
|
)
|
||||||
assert resp.status_code == 403
|
assert resp.status_code == 403
|
||||||
assert event.tax_rules.exists()
|
assert event.tax_rules.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rule_update_invalid_rules(token_client, organizer, event, taxrule):
|
||||||
|
resp = token_client.patch(
|
||||||
|
'/api/v1/organizers/{}/events/{}/taxrules/{}/'.format(organizer.slug, event.slug, taxrule.pk),
|
||||||
|
{
|
||||||
|
"custom_rules": [
|
||||||
|
{"foo": "bar"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.data["custom_rules"][0].startswith(
|
||||||
|
"Your set of rules is not valid. Error message: 'country' is a required property"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user