forked from CGM_Public/pretix_original
@@ -19,5 +19,6 @@ from .orders import (
|
||||
generate_secret,
|
||||
)
|
||||
from .organizer import Organizer, Organizer_SettingsStore, Team, TeamInvite
|
||||
from .tax import TaxRule
|
||||
from .vouchers import Voucher
|
||||
from .waitinglist import WaitingListEntry
|
||||
|
||||
@@ -304,6 +304,13 @@ class Event(EventMixin, LoggedModel):
|
||||
self.is_public = other.is_public
|
||||
self.save()
|
||||
|
||||
tax_map = {}
|
||||
for t in other.tax_rules.all():
|
||||
tax_map[t.pk] = t
|
||||
t.pk = None
|
||||
t.event = self
|
||||
t.save()
|
||||
|
||||
category_map = {}
|
||||
for c in ItemCategory.objects.filter(event=other):
|
||||
category_map[c.pk] = c
|
||||
@@ -322,6 +329,8 @@ class Event(EventMixin, LoggedModel):
|
||||
i.picture.save(i.picture.name, i.picture)
|
||||
if i.category_id:
|
||||
i.category = category_map[i.category_id]
|
||||
if i.tax_rule_id:
|
||||
i.tax_rule = tax_map[i.tax_rule_id]
|
||||
i.save()
|
||||
for v in vars:
|
||||
variation_map[v.pk] = v
|
||||
@@ -371,7 +380,18 @@ class Event(EventMixin, LoggedModel):
|
||||
)
|
||||
newname = default_storage.save(fname, fi)
|
||||
s.value = 'file://' + newname
|
||||
s.save()
|
||||
s.save()
|
||||
elif s.key == 'tax_rate_default':
|
||||
try:
|
||||
if int(s.value) in tax_map:
|
||||
s.value = tax_map.get(int(s.value)).pk
|
||||
s.save()
|
||||
else:
|
||||
s.delete()
|
||||
except ValueError:
|
||||
s.delete()
|
||||
else:
|
||||
s.save()
|
||||
|
||||
event_copy_data.send(sender=self, other=other)
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ class Invoice(models.Model):
|
||||
:type payment_provider_text: str
|
||||
:param footer_text: A footer text, displayed smaller and centered on every page
|
||||
:type footer_text: str
|
||||
:param foreign_currency_display: A different currency that taxes should also be displayed in.
|
||||
:type foreign_currency_display: str
|
||||
:param foreign_currency_rate: The rate of a forein currency that the taxes should be displayed in.
|
||||
:type foreign_currency_rate: Decimal
|
||||
:param foreign_currency_rate_date: The date of the forein currency exchange rates.
|
||||
:type foreign_currency_rate_date: date
|
||||
:param file: The filename of the rendered invoice
|
||||
:type file: File
|
||||
"""
|
||||
@@ -71,6 +77,9 @@ class Invoice(models.Model):
|
||||
additional_text = models.TextField(blank=True)
|
||||
payment_provider_text = models.TextField(blank=True)
|
||||
footer_text = models.TextField(blank=True)
|
||||
foreign_currency_display = models.CharField(max_length=50, null=True, blank=True)
|
||||
foreign_currency_rate = models.DecimalField(decimal_places=4, max_digits=10, null=True, blank=True)
|
||||
foreign_currency_rate_date = models.DateField(null=True, blank=True)
|
||||
file = models.FileField(null=True, blank=True, upload_to=invoice_filename)
|
||||
|
||||
@staticmethod
|
||||
@@ -155,12 +164,15 @@ class InvoiceLine(models.Model):
|
||||
:type tax_value: decimal.Decimal
|
||||
:param tax_rate: The applied tax rate in percent
|
||||
:type tax_rate: decimal.Decimal
|
||||
:param tax_name: The name of the applied tax rate
|
||||
:type tax_name: str
|
||||
"""
|
||||
invoice = models.ForeignKey('Invoice', related_name='lines')
|
||||
description = models.TextField()
|
||||
gross_value = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
tax_value = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_rate = models.DecimalField(max_digits=7, decimal_places=2, default=Decimal('0.00'))
|
||||
tax_name = models.CharField(max_length=190)
|
||||
|
||||
@property
|
||||
def net_value(self):
|
||||
|
||||
@@ -13,8 +13,8 @@ from django.utils.timezone import now
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
from pretix.base.models.tax import TaxedPrice
|
||||
|
||||
from .event import Event, SubEvent
|
||||
|
||||
@@ -202,10 +202,11 @@ class Item(LoggedModel):
|
||||
"additional donations for your event. This is currently not supported for products that are "
|
||||
"bought as an add-on to other products.")
|
||||
)
|
||||
tax_rate = models.DecimalField(
|
||||
verbose_name=_("Taxes included in percent"),
|
||||
max_digits=7, decimal_places=2,
|
||||
default=Decimal('0.00')
|
||||
tax_rule = models.ForeignKey(
|
||||
'TaxRule',
|
||||
verbose_name=_('Sales tax'),
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
admission = models.BooleanField(
|
||||
verbose_name=_("Is an admission ticket"),
|
||||
@@ -286,10 +287,12 @@ class Item(LoggedModel):
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
@property
|
||||
def default_price_net(self):
|
||||
tax_value = round_decimal(self.default_price * (1 - 100 / (100 + self.tax_rate)))
|
||||
return self.default_price - tax_value
|
||||
def tax(self, price=None, base_price_is='auto'):
|
||||
price = price if price is not None else self.default_price
|
||||
if not self.tax_rule:
|
||||
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'), name='')
|
||||
return self.tax_rule.tax(price, base_price_is=base_price_is)
|
||||
|
||||
def is_available(self, now_dt: datetime=None) -> bool:
|
||||
"""
|
||||
@@ -396,10 +399,11 @@ class ItemVariation(models.Model):
|
||||
def price(self):
|
||||
return self.default_price if self.default_price is not None else self.item.default_price
|
||||
|
||||
@property
|
||||
def net_price(self):
|
||||
tax_value = round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
|
||||
return self.price - tax_value
|
||||
def tax(self, price=None):
|
||||
price = price or self.price
|
||||
if not self.item.tax_rule:
|
||||
return TaxedPrice(gross=price, net=price, tax=Decimal('0.00'), rate=Decimal('0.00'), name='')
|
||||
return self.item.tax_rule.tax(price)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
@@ -8,8 +8,6 @@ from django.utils.functional import cached_property
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
|
||||
from pretix.base.models.event import SubEvent
|
||||
|
||||
|
||||
class LogEntry(models.Model):
|
||||
"""
|
||||
@@ -52,7 +50,7 @@ class LogEntry(models.Model):
|
||||
|
||||
@cached_property
|
||||
def display_object(self):
|
||||
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event
|
||||
from . import Order, Voucher, Quota, Item, ItemCategory, Question, Event, TaxRule, SubEvent
|
||||
|
||||
if self.content_type.model_class() is Event:
|
||||
return ''
|
||||
@@ -131,6 +129,16 @@ class LogEntry(models.Model):
|
||||
}),
|
||||
'val': escape(co.question),
|
||||
}
|
||||
elif isinstance(co, TaxRule):
|
||||
a_text = _('Tax rule {val}')
|
||||
a_map = {
|
||||
'href': reverse('control:event.settings.tax.edit', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
'rule': co.id
|
||||
}),
|
||||
'val': escape(co.name),
|
||||
}
|
||||
|
||||
if a_text and a_map:
|
||||
a_map['val'] = '<a href="{href}">{val}</a>'.format_map(a_map)
|
||||
|
||||
@@ -25,7 +25,6 @@ from pretix.base.i18n import language
|
||||
from pretix.base.models import User
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
|
||||
from ..decimal import round_decimal
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
from .items import Item, ItemVariation, Question, QuestionOption, Quota
|
||||
@@ -162,6 +161,11 @@ class Order(LoggedModel):
|
||||
decimal_places=2, max_digits=10,
|
||||
default=0, verbose_name=_("Payment method fee tax")
|
||||
)
|
||||
payment_fee_tax_rule = models.ForeignKey(
|
||||
'TaxRule',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
payment_info = models.TextField(
|
||||
verbose_name=_("Payment information"),
|
||||
null=True, blank=True
|
||||
@@ -229,12 +233,26 @@ class Order(LoggedModel):
|
||||
Calculates the taxes on the payment fees and sets the parameters payment_fee_tax_rate
|
||||
and payment_fee_tax_value accordingly.
|
||||
"""
|
||||
self.payment_fee_tax_rate = self.event.settings.get('tax_rate_default')
|
||||
if self.payment_fee_tax_rate:
|
||||
self.payment_fee_tax_value = round_decimal(
|
||||
self.payment_fee * (1 - 100 / (100 + self.payment_fee_tax_rate)))
|
||||
if self.event.settings.tax_rate_default:
|
||||
tr = self.event.settings.tax_rate_default
|
||||
tax = tr.tax(self.payment_fee, base_price_is='gross')
|
||||
rate, tax = tax.rate, tax.tax
|
||||
|
||||
try:
|
||||
ia = self.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
if not tr.tax_applicable(ia):
|
||||
rate = 0
|
||||
tax = 0
|
||||
|
||||
self.payment_fee_tax_rate = rate
|
||||
self.payment_fee_tax_value = tax
|
||||
self.payment_fee_tax_rule = tr
|
||||
else:
|
||||
self.payment_fee_tax_rate = Decimal('0.00')
|
||||
self.payment_fee_tax_value = Decimal('0.00')
|
||||
self.payment_fee_tax_rule = None
|
||||
|
||||
@property
|
||||
def payment_fee_net(self):
|
||||
@@ -671,6 +689,15 @@ class OrderPosition(AbstractPosition):
|
||||
max_digits=7, decimal_places=2,
|
||||
verbose_name=_('Tax rate')
|
||||
)
|
||||
tax_rule = models.ForeignKey(
|
||||
'TaxRule',
|
||||
on_delete=models.PROTECT,
|
||||
null=True, blank=True
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
)
|
||||
tax_value = models.DecimalField(
|
||||
max_digits=10, decimal_places=2,
|
||||
verbose_name=_('Tax value')
|
||||
@@ -730,11 +757,22 @@ class OrderPosition(AbstractPosition):
|
||||
)
|
||||
|
||||
def _calculate_tax(self):
|
||||
self.tax_rate = self.item.tax_rate
|
||||
if self.tax_rate:
|
||||
self.tax_value = round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
|
||||
self.tax_rule = self.item.tax_rule
|
||||
try:
|
||||
ia = self.order.invoice_address
|
||||
except InvoiceAddress.DoesNotExist:
|
||||
ia = None
|
||||
if self.tax_rule:
|
||||
if self.tax_rule.tax_applicable(ia):
|
||||
tax = self.tax_rule.tax(self.price, base_price_is='gross')
|
||||
self.tax_rate = tax.rate
|
||||
self.tax_value = tax.tax
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_rate = Decimal('0.00')
|
||||
else:
|
||||
self.tax_value = Decimal('0.00')
|
||||
self.tax_rate = Decimal('0.00')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.tax_rate is None:
|
||||
@@ -775,6 +813,9 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_("Expiration date"),
|
||||
db_index=True
|
||||
)
|
||||
includes_tax = models.BooleanField(
|
||||
default=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Cart position")
|
||||
@@ -787,19 +828,23 @@ class CartPosition(AbstractPosition):
|
||||
|
||||
@property
|
||||
def tax_rate(self):
|
||||
return self.item.tax_rate
|
||||
if self.includes_tax:
|
||||
return self.item.tax(self.price, base_price_is='gross').rate
|
||||
else:
|
||||
return Decimal('0.00')
|
||||
|
||||
@property
|
||||
def tax_value(self):
|
||||
if not self.tax_rate:
|
||||
if self.includes_tax:
|
||||
return self.item.tax(self.price, base_price_is='gross').tax
|
||||
else:
|
||||
return Decimal('0.00')
|
||||
return round_decimal(self.price * (1 - 100 / (100 + self.item.tax_rate)))
|
||||
|
||||
|
||||
class InvoiceAddress(models.Model):
|
||||
last_modified = models.DateTimeField(auto_now=True)
|
||||
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
|
||||
order = models.OneToOneField(Order, null=True, blank=True, related_name='invoice_address')
|
||||
is_business = models.BooleanField(default=False, verbose_name=_('Business customer'))
|
||||
company = models.CharField(max_length=255, blank=True, verbose_name=_('Company name'))
|
||||
name = models.CharField(max_length=255, verbose_name=_('Full name'), blank=True)
|
||||
street = models.TextField(verbose_name=_('Address'), blank=False)
|
||||
@@ -809,6 +854,7 @@ class InvoiceAddress(models.Model):
|
||||
country = CountryField(verbose_name=_('Country'), blank=False, blank_label=_('Select country'))
|
||||
vat_id = models.CharField(max_length=255, blank=True, verbose_name=_('VAT ID'),
|
||||
help_text=_('Only for business customers within the EU.'))
|
||||
vat_id_validated = models.BooleanField(default=False)
|
||||
|
||||
|
||||
def cachedticket_name(instance, filename: str) -> str:
|
||||
|
||||
174
src/pretix/base/models/tax.py
Normal file
174
src/pretix/base/models/tax.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.utils.formats import localize
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django_countries.fields import CountryField
|
||||
from i18nfield.fields import I18nCharField
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
|
||||
class TaxedPrice:
|
||||
def __init__(self, *, gross: Decimal, net: Decimal, tax: Decimal, rate: Decimal, name: str):
|
||||
if net + tax != gross:
|
||||
raise ValueError('Net value and tax value need to add to the gross value')
|
||||
self.gross = gross
|
||||
self.net = net
|
||||
self.tax = tax
|
||||
self.rate = rate
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return '{} + {}% = {}'.format(localize(self.net), localize(self.rate), localize(self.gross))
|
||||
|
||||
|
||||
TAXED_ZERO = TaxedPrice(
|
||||
gross=Decimal('0.00'),
|
||||
net=Decimal('0.00'),
|
||||
tax=Decimal('0.00'),
|
||||
rate=Decimal('0.00'),
|
||||
name=''
|
||||
)
|
||||
|
||||
EU_COUNTRIES = {
|
||||
'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT',
|
||||
'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB'
|
||||
}
|
||||
EU_CURRENCIES = {
|
||||
'BG': 'BGN',
|
||||
'GB': 'GBP',
|
||||
'HR': 'HRK',
|
||||
'CZ': 'CZK',
|
||||
'DK': 'DKK',
|
||||
'HU': 'HUF',
|
||||
'PL': 'PLN',
|
||||
'RO': 'RON',
|
||||
'SE': 'SEK'
|
||||
}
|
||||
|
||||
|
||||
class TaxRule(LoggedModel):
|
||||
event = models.ForeignKey('Event', related_name='tax_rules')
|
||||
name = I18nCharField(
|
||||
verbose_name=_('Name'),
|
||||
help_text=_('Should be short, e.g. "VAT"'),
|
||||
max_length=190,
|
||||
)
|
||||
rate = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
verbose_name=_("Tax rate")
|
||||
)
|
||||
price_includes_tax = models.BooleanField(
|
||||
verbose_name=_("The configured product prices include the tax amount"),
|
||||
default=True,
|
||||
)
|
||||
eu_reverse_charge = models.BooleanField(
|
||||
verbose_name=_("Use EU reverse charge taxation rules"),
|
||||
default=False,
|
||||
help_text=_("Not recommended. Most events will NOT be qualified for reverse charge since the place of "
|
||||
"taxation is the location of the event. This option disables charging VAT for all customers "
|
||||
"outside the EU and for business customers in different EU countries that do not "
|
||||
"customers who entered a valid EU VAT ID. Only enable this option after consulting a tax counsel. "
|
||||
"No warranty given for correct tax calculation. USE AT YOUR OWN RISK.")
|
||||
)
|
||||
home_country = CountryField(
|
||||
verbose_name=_('Merchant country'),
|
||||
blank=True,
|
||||
help_text=_('Your country of residence. This is the country the EU reverse charge rule will not apply in, '
|
||||
'if configured above.'),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def zero(cls):
|
||||
return cls(
|
||||
event=None,
|
||||
name='',
|
||||
rate=Decimal('0.00'),
|
||||
price_includes_tax=True,
|
||||
eu_reverse_charge=False
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
if self.eu_reverse_charge and not self.home_country:
|
||||
raise ValueError(_('You need to set your home country to use the reverse charge feature.'))
|
||||
|
||||
def __str__(self):
|
||||
if self.price_includes_tax:
|
||||
s = _('incl. {rate} {name}').format(rate=self.rate, name=self.name)
|
||||
else:
|
||||
s = _('plus {rate} {name}').format(rate=self.rate, name=self.name)
|
||||
if self.eu_reverse_charge:
|
||||
s += ' ({})'.format(_('reverse charge enabled'))
|
||||
return str(s)
|
||||
|
||||
def tax(self, base_price, base_price_is='auto'):
|
||||
if self.rate == Decimal('0.00'):
|
||||
return TaxedPrice(
|
||||
net=base_price, gross=base_price, tax=Decimal('0.00'),
|
||||
rate=self.rate, name=self.name
|
||||
)
|
||||
|
||||
if base_price_is == 'auto':
|
||||
if self.price_includes_tax:
|
||||
base_price_is = 'gross'
|
||||
else:
|
||||
base_price_is = 'net'
|
||||
|
||||
if base_price_is == 'gross':
|
||||
gross = base_price
|
||||
net = gross - round_decimal(base_price * (1 - 100 / (100 + self.rate)))
|
||||
elif base_price_is == 'net':
|
||||
net = base_price
|
||||
gross = round_decimal(net * (1 + self.rate / 100))
|
||||
else:
|
||||
raise ValueError('Unknown base price type: {}'.format(base_price_is))
|
||||
|
||||
return TaxedPrice(
|
||||
net=net, gross=gross, tax=gross - net,
|
||||
rate=self.rate, name=self.name
|
||||
)
|
||||
|
||||
def is_reverse_charge(self, invoice_address):
|
||||
if not self.eu_reverse_charge:
|
||||
return False
|
||||
|
||||
if not invoice_address or not invoice_address.country:
|
||||
return False
|
||||
|
||||
if str(invoice_address.country) not in EU_COUNTRIES:
|
||||
return False
|
||||
|
||||
if invoice_address.country == self.home_country:
|
||||
return False
|
||||
|
||||
if invoice_address.is_business and invoice_address.vat_id and invoice_address.vat_id_validated:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def tax_applicable(self, invoice_address):
|
||||
if not self.eu_reverse_charge:
|
||||
# No reverse charge rules? Always apply VAT!
|
||||
return True
|
||||
|
||||
if not invoice_address or not invoice_address.country:
|
||||
# No country specified? Always apply VAT!
|
||||
return True
|
||||
|
||||
if str(invoice_address.country) not in EU_COUNTRIES:
|
||||
# Non-EU country? Never apply VAT!
|
||||
return False
|
||||
|
||||
if invoice_address.country == self.home_country:
|
||||
# Within same EU country? Always apply VAT!
|
||||
return True
|
||||
|
||||
if invoice_address.is_business and invoice_address.vat_id and invoice_address.vat_id_validated:
|
||||
# Reverse charge case
|
||||
return False
|
||||
|
||||
# Consumer in different EU country / invalid VAT
|
||||
return True
|
||||
Reference in New Issue
Block a user