New concept for fee handling (#610)

* New concept for fee handling

* More usages

* Remove all usages, make all tests pass

* API changes

* Small fixes

* Fix order of invoice lines

* Rebase migration
This commit is contained in:
Raphael Michel
2017-09-05 10:11:26 +03:00
committed by GitHub
parent a2a88cfafa
commit e54e0d6511
26 changed files with 568 additions and 227 deletions
+32
View File
@@ -6067,3 +6067,35 @@ url('../opensans_regular_macroman/OpenSans-Regular-webfont.svg#open_sansregular'
img.screenshot, a.screenshot img {
box-shadow: 0 4px 18px 0 rgba(0,0,0,0.1), 0 6px 20px 0 rgba(0,0,0,0.09);
}
/* Changes */
.versionchanged {
background: #e7f2fa;
padding: 12px;
line-height: 24px;
margin-bottom: 24px;
-webkit-font-smoothing: antialiased;
}
.versionmodified {
background: #6ab0de;
font-weight: bold;
display: block;
color: #fff;
margin: -12px;
padding: 6px 12px;
margin-bottom: 12px;
font-family: inherit;
}
.versionmodified:before {
font-family: "FontAwesome";
display: inline-block;
font-style: normal;
font-weight: normal;
line-height: 1;
text-decoration: inherit;
content: "";
margin-right: 4px;
}
.versionchanged p:last-child {
margin-bottom: 0;
}
+16 -10
View File
@@ -48,6 +48,17 @@ invoice_address object Invoice address
EU VAT service and validation was successful. This only
happens in rare cases.
position list of objects List of order positions (see below)
fees list of objects List of fees included in the order total (i.e.
payment fees)
├ fee_type string Type of fee (currently ``payment``, ``passbook``,
``other``)
├ value money (string) Fee amount
├ description string Human-readable string with more details (can be empty)
├ internal_type string Internal string (i.e. ID of the payment provider),
can be empty
├ tax_rate decimal (string) VAT rate applied for this fee
├ tax_value money (string) VAT included in this fee
└ tax_rule integer The ID of the used tax rule (or ``null``)
downloads list of objects List of ticket download options for order-wise ticket
downloading. This might be a multi-page PDF or a ZIP
file of tickets for outputs that do not support
@@ -65,8 +76,9 @@ downloads list of objects List of ticket
.. versionchanged:: 1.7
The attributes ``payment_fee_tax_rule``, ``invoice_address.vat_id_validated`` and ``invoice_address.is_business``
have been added.
The attributes ``invoice_address.vat_id_validated`` and ``invoice_address.is_business`` have been added.
The attributes ``order.payment_fee``, ``order.payment_fee_tax_rate`` and ``order.payment_fee_tax_value`` have been
deprecated in favour of the new ``fees`` attribute but will still be served and removed in 1.9.
Order position resource
@@ -146,10 +158,7 @@ Order endpoints
"expires": "2017-12-10T10:00:00Z",
"payment_date": "2017-12-05",
"payment_provider": "banktransfer",
"payment_fee": "0.00",
"payment_fee_tax_rate": "0.00",
"payment_fee_tax_value": "0.00",
"payment_fee_tax_rule": null,
"fees": [],
"total": "23.00",
"comment": "",
"invoice_address": {
@@ -254,10 +263,7 @@ Order endpoints
"expires": "2017-12-10T10:00:00Z",
"payment_date": "2017-12-05",
"payment_provider": "banktransfer",
"payment_fee": "0.00",
"payment_fee_tax_rate": "0.00",
"payment_fee_tax_value": "0.00",
"payment_fee_tax_rule": null,
"fees": [],
"total": "23.00",
"comment": "",
"invoice_address": {
+27 -2
View File
@@ -1,3 +1,5 @@
from decimal import Decimal
from rest_framework import serializers
from rest_framework.reverse import reverse
@@ -6,6 +8,7 @@ from pretix.base.models import (
Checkin, Invoice, InvoiceAddress, InvoiceLine, Order, OrderPosition,
QuestionAnswer,
)
from pretix.base.models.orders import OrderFee
from pretix.base.signals import register_ticket_outputs
@@ -101,16 +104,38 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
'answers', 'tax_rule')
class OrderFeeSerializer(I18nAwareModelSerializer):
class Meta:
model = OrderFee
fields = ('fee_type', 'value', 'description', 'internal_type', 'tax_rate', 'tax_value', 'tax_rule')
class PaymentFeeLegacyField(serializers.Field):
def __init__(self, *args, **kwargs):
self.attr = kwargs.pop('attribute')
super().__init__(*args, **kwargs)
def to_representation(self, instance: Order):
return str(
sum([getattr(f, self.attr) for f in instance.fees.all() if f.fee_type == OrderFee.FEE_TYPE_PAYMENT],
Decimal('0.00'))
)
class OrderSerializer(I18nAwareModelSerializer):
invoice_address = InvoiceAdddressSerializer()
positions = OrderPositionSerializer(many=True)
fees = OrderFeeSerializer(many=True)
downloads = OrderDownloadsField(source='*')
payment_fee = PaymentFeeLegacyField(source='*', attribute='value') # TODO: Remove in 1.9
payment_fee_tax_rate = PaymentFeeLegacyField(source='*', attribute='tax_rate') # TODO: Remove in 1.9
payment_fee_tax_value = PaymentFeeLegacyField(source='*', attribute='tax_value') # TODO: Remove in 1.9
class Meta:
model = Order
fields = ('code', 'status', 'secret', 'email', 'locale', 'datetime', 'expires', 'payment_date',
'payment_provider', 'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value',
'payment_fee_tax_rule', 'total', 'comment', 'invoice_address', 'positions', 'downloads')
'payment_provider', 'fees', 'total', 'comment', 'invoice_address', 'positions', 'downloads',
'payment_fee', 'payment_fee_tax_rate', 'payment_fee_tax_value')
class InlineInvoiceLineSerializer(I18nAwareModelSerializer):
+2 -1
View File
@@ -37,7 +37,8 @@ class OrderViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
return self.request.event.orders.prefetch_related(
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options'
'positions', 'positions__checkins', 'positions__item', 'positions__answers', 'positions__answers__options',
'fees'
).select_related(
'invoice_address'
)
+8 -2
View File
@@ -61,7 +61,13 @@ class JSONExporter(BaseExporter):
'status': order.status,
'user': order.email,
'datetime': order.datetime,
'payment_fee': order.payment_fee,
'fees': [
{
'type': fee.fee_type,
'description': fee.description,
'value': fee.value,
} for fee in order.fees.all()
],
'total': order.total,
'positions': [
{
@@ -82,7 +88,7 @@ class JSONExporter(BaseExporter):
} for position in order.positions.all()
]
} for order in
self.event.orders.all().prefetch_related('positions', 'positions__answers')
self.event.orders.all().prefetch_related('positions', 'positions__answers', 'fees')
],
'quotas': [
{
+22 -9
View File
@@ -11,6 +11,7 @@ from django.utils.formats import localize
from django.utils.translation import ugettext as _, ugettext_lazy
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee
from ..exporter import BaseExporter
from ..signals import register_data_exporters
@@ -35,7 +36,10 @@ class OrderListExporter(BaseExporter):
def _get_all_tax_rates(self, qs):
tax_rates = set(
qs.exclude(payment_fee=0).values_list('payment_fee_tax_rate', flat=True).distinct().order_by()
a for a
in OrderFee.objects.filter(
order__event=self.event
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates |= set(
a for a
@@ -59,7 +63,7 @@ class OrderListExporter(BaseExporter):
headers = [
_('Order code'), _('Order total'), _('Status'), _('Email'), _('Order date'),
_('Company'), _('Name'), _('Address'), _('ZIP code'), _('City'), _('Country'), _('VAT ID'),
_('Payment date'), _('Payment type'), _('Payment method fee'),
_('Payment date'), _('Payment type'), _('Fees'),
]
for tr in tax_rates:
@@ -78,6 +82,16 @@ class OrderListExporter(BaseExporter):
for k, v in self.event.get_payment_providers().items()
}
full_fee_sum_cache = {
o['order__id']: o['grosssum'] for o in
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(grosssum=Sum('value'))
}
fee_sum_cache = {
(o['order__id'], o['tax_rate']): o for o in
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(
taxsum=Sum('tax_value'), grosssum=Sum('value')
)
}
sum_cache = {
(o['order__id'], o['tax_rate']): o for o in
OrderPosition.objects.values('tax_rate', 'order__id').order_by().annotate(
@@ -109,19 +123,18 @@ class OrderListExporter(BaseExporter):
row += [
order.payment_date.astimezone(tz).strftime('%Y-%m-%d') if order.payment_date else '',
provider_names.get(order.payment_provider, order.payment_provider),
localize(order.payment_fee)
localize(full_fee_sum_cache.get(order.id) or Decimal('0.00'))
]
for tr in tax_rates:
taxrate_values = sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
if tr == order.payment_fee_tax_rate and order.payment_fee_tax_value:
taxrate_values['grosssum'] += order.payment_fee
taxrate_values['taxsum'] += order.payment_fee_tax_value
fee_taxrate_values = fee_sum_cache.get((order.id, tr), {'grosssum': Decimal('0.00'), 'taxsum': Decimal('0.00')})
row += [
localize(taxrate_values['grosssum']),
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']),
localize(taxrate_values['taxsum']),
localize(taxrate_values['grosssum'] + fee_taxrate_values['grosssum']),
localize(taxrate_values['grosssum'] - taxrate_values['taxsum']
+ fee_taxrate_values['grosssum'] - fee_taxrate_values['taxsum']),
localize(taxrate_values['taxsum'] + fee_taxrate_values['taxsum']),
]
row.append(', '.join([i.number for i in order.invoices.all()]))
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-28 14:35
from __future__ import unicode_literals
from decimal import Decimal
import django.db.models.deletion
from django.db import migrations, models
def fee_converter(app, schema_editor):
OrderFee = app.get_model('pretixbase', 'OrderFee')
Order = app.get_model('pretixbase', 'Order')
of = []
for o in Order.objects.exclude(payment_fee=Decimal('0.00')).iterator():
of.append(OrderFee(
order=o,
value=o.payment_fee,
fee_type='payment',
tax_rate=o.payment_fee_tax_rate,
tax_rule=o.payment_fee_tax_rule,
tax_value=o.payment_fee_tax_value,
internal_type=o.payment_provider
))
if len(of) > 900:
OrderFee.objects.bulk_create(of)
of = []
OrderFee.objects.bulk_create(of)
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0075_auto_20170828_0901'),
]
operations = [
migrations.CreateModel(
name='OrderFee',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Value')),
('description', models.CharField(blank=True, max_length=190)),
('internal_type', models.CharField(blank=True, max_length=255)),
('fee_type', models.CharField(choices=[('payment', 'Payment method fee'), ('shipping', 'Shipping fee')], max_length=100)),
('tax_rate', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Tax rate')),
('tax_value', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Tax value')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='fees', to='pretixbase.Order', verbose_name='Order')),
('tax_rule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.TaxRule')),
],
),
migrations.RunPython(
fee_converter, migrations.RunPython.noop
),
migrations.RemoveField(
model_name='order',
name='payment_fee',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_rate',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_rule',
),
migrations.RemoveField(
model_name='order',
name='payment_fee_tax_value',
),
]
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-29 11:26
from __future__ import unicode_literals
from django.db import migrations, models
def assign_positions(app, schema_editor):
Invoice = app.get_model('pretixbase', 'Invoice')
for i in Invoice.objects.iterator():
for j, l in enumerate(i.lines.all()):
l.position = j
l.save()
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0076_orderfee'),
]
operations = [
migrations.AddField(
model_name='invoiceline',
name='position',
field=models.PositiveIntegerField(default=0),
),
migrations.AlterField(
model_name='orderfee',
name='fee_type',
field=models.CharField(choices=[('payment', 'Payment fee'), ('shipping', 'Shipping fee'), ('other', 'Other fees')], max_length=100),
),
migrations.RunPython(
assign_positions, migrations.RunPython.noop
),
migrations.AlterModelOptions(
name='invoiceline',
options={'ordering': ('position', 'pk')},
),
]
+4
View File
@@ -168,6 +168,7 @@ class InvoiceLine(models.Model):
:type tax_name: str
"""
invoice = models.ForeignKey('Invoice', related_name='lines')
position = models.PositiveIntegerField(default=0)
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'))
@@ -177,3 +178,6 @@ class InvoiceLine(models.Model):
@property
def net_value(self):
return self.gross_value - self.tax_value
class Meta:
ordering = ('position', 'pk')
+86 -56
View File
@@ -78,12 +78,6 @@ class Order(LoggedModel):
:type payment_date: datetime
:param payment_provider: The payment provider selected by the user
:type payment_provider: str
:param payment_fee: The payment fee calculated at checkout time
:type payment_fee: decimal.Decimal
:param payment_fee_tax_value: The absolute amount of tax included in the payment fee
:type payment_fee_tax_value: decimal.Decimal
:param payment_fee_tax_rate: The tax rate applied to the payment fee (in percent)
:type payment_fee_tax_rate: decimal.Decimal
:param payment_info: Arbitrary information stored by the payment provider
:type payment_info: str
:param total: The total amount of the order, including the payment fee
@@ -149,23 +143,6 @@ class Order(LoggedModel):
max_length=255,
verbose_name=_("Payment provider")
)
payment_fee = models.DecimalField(
decimal_places=2, max_digits=10,
default=0, verbose_name=_("Payment method fee")
)
payment_fee_tax_rate = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Payment method fee tax rate")
)
payment_fee_tax_value = models.DecimalField(
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
@@ -224,43 +201,11 @@ class Order(LoggedModel):
self.assign_code()
if not self.datetime:
self.datetime = now()
if self.payment_fee_tax_rate is None:
self._calculate_tax()
super().save(*args, **kwargs)
def _calculate_tax(self):
"""
Calculates the taxes on the payment fees and sets the parameters payment_fee_tax_rate
and payment_fee_tax_value accordingly.
"""
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):
return self.payment_fee - self.payment_fee_tax_value
@cached_property
def tax_total(self):
return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + self.payment_fee_tax_value
return (self.positions.aggregate(s=Sum('tax_value'))['s'] or 0) + (self.fees.aggregate(s=Sum('tax_value'))['s'] or 0)
@property
def net_total(self):
@@ -669,6 +614,91 @@ class AbstractPosition(models.Model):
else self.variation.quotas.filter(subevent=self.subevent))
class OrderFee(models.Model):
"""
An OrderFee objet represents a fee that is added to the order total independently of
the actual positions. This might for example be a payment or a shipping fee.
"""
FEE_TYPE_PAYMENT = "payment"
FEE_TYPE_SHIPPING = "shipping"
FEE_TYPE_OTHER = "other"
FEE_TYPES = (
(FEE_TYPE_PAYMENT, _("Payment fee")),
(FEE_TYPE_SHIPPING, _("Shipping fee")),
(FEE_TYPE_OTHER, _("Other fees")),
)
value = models.DecimalField(
decimal_places=2, max_digits=10,
verbose_name=_("Value")
)
order = models.ForeignKey(
Order,
verbose_name=_("Order"),
related_name='fees',
on_delete=models.PROTECT
)
fee_type = models.CharField(
max_length=100, choices=FEE_TYPES
)
description = models.CharField(max_length=190, blank=True)
internal_type = models.CharField(max_length=255, blank=True)
tax_rate = models.DecimalField(
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')
)
@property
def net_value(self):
return self.value - self.tax_value
def __str__(self):
if self.description:
return '{} - {}'.format(self.get_fee_type_display(), self.description)
else:
return self.get_fee_type_display()
def __repr__(self):
return '<OrderFee: type %s, value %d>' % (
self.fee_type, self.value
)
def _calculate_tax(self):
try:
ia = self.order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
if not self.tax_rule and self.fee_type == "payment" and self.order.event.settings.tax_rate_default:
self.tax_rule = self.order.event.settings.tax_rate_default
if self.tax_rule:
if self.tax_rule.tax_applicable(ia):
tax = self.tax_rule.tax(self.value, 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:
self._calculate_tax()
return super().save(*args, **kwargs)
class OrderPosition(AbstractPosition):
"""
An OrderPosition is one line of an order, representing one ordered item
+36 -1
View File
@@ -14,7 +14,8 @@ from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, ItemVariation, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice
from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import TAXED_ZERO, TaxedPrice, TaxRule
from pretix.base.services.async import ProfiledTask
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.pricing import get_price
@@ -626,6 +627,40 @@ def update_tax_rates(event: Event, cart_id: str, invoice_address: InvoiceAddress
return totaldiff
def get_fees(event, total, invoice_address, provider):
fees = []
if total == 0:
return fees
if provider:
provider = event.get_payment_providers().get(provider)
if provider:
payment_fee = provider.calculate_fee(total)
if payment_fee:
payment_fee_tax_rule = event.settings.tax_rate_default or TaxRule.zero()
if payment_fee_tax_rule.tax_applicable(invoice_address):
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross')
fees.append(OrderFee(
fee_type="PAYMENT",
value=payment_fee,
tax_rate=payment_fee_tax.rate,
tax_value=payment_fee_tax.tax,
tax_rule=payment_fee_tax_rule
))
else:
fees.append(OrderFee(
fee_type="PAYMENT",
value=payment_fee,
tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'),
tax_rule=payment_fee_tax_rule
))
return fees
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def add_items_to_cart(self, event: int, items: List[dict], cart_id: str=None, locale='en',
invoice_address: int=None) -> None:
+13 -7
View File
@@ -99,7 +99,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
reverse_charge = False
positions.sort(key=lambda p: p.sort_key)
for p in positions:
for i, p in enumerate(positions):
if not invoice.event.settings.invoice_include_free and p.price == Decimal('0.00') and not p.addon_c:
continue
@@ -109,7 +109,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
if p.addon_to_id:
desc = " + " + desc
InvoiceLine.objects.create(
invoice=invoice, description=desc,
position=i, invoice=invoice, description=desc,
gross_value=p.price, tax_value=p.tax_value,
tax_rate=p.tax_rate, tax_name=p.tax_rule.name if p.tax_rule else ''
)
@@ -127,13 +127,19 @@ def build_invoice(invoice: Invoice) -> Invoice:
)
invoice.save()
if invoice.order.payment_fee:
offset = len(positions)
for i, fee in enumerate(invoice.order.fees.all()):
fee_title = _(fee.get_fee_type_display())
if fee.description:
fee_title += " - " + fee.description
InvoiceLine.objects.create(
position=i + offset,
invoice=invoice,
description=_('Payment via {method}').format(method=str(payment_provider.verbose_name)),
gross_value=invoice.order.payment_fee, tax_value=invoice.order.payment_fee_tax_value,
tax_rate=invoice.order.payment_fee_tax_rate,
tax_name=invoice.order.payment_fee_tax_rule.name if invoice.order.payment_fee_tax_rule else ''
description=fee_title,
gross_value=fee.value,
tax_value=fee.tax_value,
tax_rate=fee.tax_rate,
tax_name=fee.tax_rule.name if fee.tax_rule else ''
)
return invoice
+28 -10
View File
@@ -23,7 +23,7 @@ from pretix.base.models import (
User, Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import CachedTicket, InvoiceAddress
from pretix.base.models.orders import CachedTicket, InvoiceAddress, OrderFee
from pretix.base.models.tax import TaxedPrice
from pretix.base.payment import BasePaymentProvider
from pretix.base.reldate import RelativeDateWrapper
@@ -342,14 +342,23 @@ def _check_positions(event: Event, now_dt: datetime, positions: List[CartPositio
raise OrderError(err, errargs)
def _get_fees(positions: List[CartPosition], payment_provider: BasePaymentProvider):
fees = []
total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total)
if payment_fee:
fees.append(OrderFee(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=payment_fee))
return fees
def _create_order(event: Event, email: str, positions: List[CartPosition], now_dt: datetime,
payment_provider: BasePaymentProvider, locale: str=None, address: int=None,
meta_info: dict=None):
from datetime import time
total = sum([c.price for c in positions])
payment_fee = payment_provider.calculate_fee(total)
total += payment_fee
fees = _get_fees(positions, payment_provider)
total = sum([c.price for c in positions]) + sum([c.value for c in fees])
tz = pytz.timezone(event.settings.timezone)
exp_by_date = now_dt.astimezone(tz) + timedelta(days=event.settings.get('payment_term_days', as_type=int))
@@ -387,7 +396,6 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
expires=expires,
locale=locale,
total=total,
payment_fee=payment_fee,
payment_provider=payment_provider.identifier,
meta_info=json.dumps(meta_info or {}),
)
@@ -398,9 +406,13 @@ def _create_order(event: Event, email: str, positions: List[CartPosition], now_d
address.order = order
address.save()
order._calculate_tax() # Might have changed due to new invoice address
order.save()
for fee in fees:
fee.order = order
fee._calculate_tax()
fee.save()
OrderPosition.transform_cart_positions(positions, order)
order.log_action('pretix.event.order.placed')
@@ -825,15 +837,21 @@ class OrderChangeManager:
})
def _recalculate_total_and_payment_fee(self):
self.order.total = sum([p.price for p in self.order.positions.all()])
payment_fee = Decimal('0.00')
if self.order.total != 0:
prov = self._get_payment_provider()
if prov:
payment_fee = prov.calculate_fee(self.order.total)
self.order.payment_fee = payment_fee
self.order.total += payment_fee
self.order._calculate_tax()
if payment_fee:
fee = self.order.fees.get_or_create(fee_type=OrderFee.FEE_TYPE_PAYMENT, defaults={'value': 0})[0]
fee.value = payment_fee
fee._calculate_tax()
fee.save()
else:
self.order.fees.filter(fee_type=OrderFee.FEE_TYPE_PAYMENT).delete()
self.order.total = sum([p.price for p in self.order.positions.all()]) + sum([f.value for f in self.order.fees.all()])
self.order.save()
def _reissue_invoice(self):
+23 -16
View File
@@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import Event, Item, ItemCategory, Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
class DummyObject:
@@ -157,33 +158,35 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It
# Payment fees
payment_cat_obj = DummyObject()
payment_cat_obj.name = _('Payment method fees')
payment_cat_obj.name = _('Fees')
payment_items = []
if not subevent:
counters = event.orders.values('payment_provider', 'status').annotate(
cnt=Count('id'), payment_fee=Sum('payment_fee'), tax_value=Sum('payment_fee_tax_value')
).order_by()
counters = OrderFee.objects.filter(
order__event=event
).values(
'fee_type', 'internal_type', 'order__status'
).annotate(cnt=Count('id'), value=Sum('value'), tax_value=Sum('tax_value')).order_by()
num_canceled = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_CANCELED
(o['fee_type'], o['internal_type']): (o['cnt'], o['value'], o['value'] - o['tax_value'])
for o in counters if o['order__status'] == Order.STATUS_CANCELED
}
num_refunded = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_REFUNDED
(o['fee_type'], o['internal_type']): (o['cnt'], o['value'], o['value'] - o['tax_value'])
for o in counters if o['order__status'] == Order.STATUS_REFUNDED
}
num_pending = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_PENDING
(o['fee_type'], o['internal_type']): (o['cnt'], o['value'], o['value'] - o['tax_value'])
for o in counters if o['order__status'] == Order.STATUS_PENDING
}
num_expired = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_EXPIRED
(o['fee_type'], o['internal_type']): (o['cnt'], o['value'], o['value'] - o['tax_value'])
for o in counters if o['order__status'] == Order.STATUS_EXPIRED
}
num_paid = {
o['payment_provider']: (o['cnt'], o['payment_fee'], o['payment_fee'] - o['tax_value'])
for o in counters if o['status'] == Order.STATUS_PAID
(o['fee_type'], o['internal_type']): (o['cnt'], o['value'], o['value'] - o['tax_value'])
for o in counters if o['order__status'] == Order.STATUS_PAID
}
num_total = dictsum(num_pending, num_paid)
@@ -191,11 +194,15 @@ def order_overview(event: Event, subevent: SubEvent=None) -> Tuple[List[Tuple[It
k: v.verbose_name
for k, v in event.get_payment_providers().items()
}
names = dict(OrderFee.FEE_TYPES)
for pprov, total in num_total.items():
ppobj = DummyObject()
ppobj.name = provider_names.get(pprov, pprov)
ppobj.provider = pprov
if pprov[0] == OrderFee.FEE_TYPE_PAYMENT:
ppobj.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], provider_names.get(pprov[1], pprov[1]))
else:
ppobj.name = '{} - {}'.format(names[OrderFee.FEE_TYPE_PAYMENT], pprov[1])
ppobj.provider = pprov[1]
ppobj.has_variations = False
ppobj.num_total = total
ppobj.num_canceled = num_canceled.get(pprov, (0, 0, 0))
@@ -266,28 +266,31 @@
<div class="clearfix"></div>
</div>
{% endfor %}
{% if items.payment_fee %}
{% for fee in items.fees %}
<div class="row-fluid product-row">
<div class="col-md-4 col-xs-6">
<strong>{% trans "Payment method fee" %}</strong>
<strong>{{ fee.get_fee_type_display }}</strong>
{% if fee.description %}
{{ fee.description }}
{% endif %}
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
{% if event.settings.display_net_prices %}
<strong>{{ event.currency }} {{ order.payment_fee_net|floatformat:2 }}</strong>
{% if order.payment_fee_tax_rate %}
<strong>{{ event.currency }} {{ fee.net_value|floatformat:2 }}</strong>
{% if fee.tax_rate %}
<br/>
<small>
{% blocktrans trimmed with rate=order.payment_fee_tax_rate taxname=order.payment_fee_tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate taxname=fee.tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
{% else %}
<strong>{{ event.currency }} {{ items.payment_fee|floatformat:2 }}</strong>
{% if order.payment_fee_tax_rate %}
<strong>{{ event.currency }} {{ fee.value|floatformat:2 }}</strong>
{% if fee.tax_rate %}
<br/>
<small>
{% blocktrans trimmed with rate=order.payment_fee_tax_rate taxname=order.payment_fee_tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate taxname=fee.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -296,7 +299,7 @@
</div>
<div class="clearfix"></div>
</div>
{% endif %}
{% endfor %}
{% if event.settings.display_net_prices and items.net_total %}
<div class="row-fluid product-row total">
<div class="col-md-4 col-xs-6">
+2 -1
View File
@@ -29,6 +29,7 @@ from pretix.base.models import (
Order, OrderPosition, RequiredAction, TaxRule, Voucher,
)
from pretix.base.models.event import EventMetaValue
from pretix.base.models.orders import OrderFee
from pretix.base.services import tickets
from pretix.base.services.invoices import build_preview_invoice_pdf
from pretix.base.signals import event_live_issues, register_ticket_outputs
@@ -957,7 +958,7 @@ class TaxDelete(EventPermissionRequiredMixin, DeleteView):
def is_allowed(self) -> bool:
o = self.object
return (
not self.request.event.orders.filter(payment_fee_tax_rule=o).exists()
not OrderFee.objects.filter(tax_rule=o, order__event=self.request.event).exists()
and not OrderPosition.objects.filter(tax_rule=o, order__event=self.request.event).exists()
and not self.request.event.items.filter(tax_rule=o).exists()
and self.request.event.settings.tax_rate_default != o
+3 -2
View File
@@ -27,6 +27,7 @@ from pretix.base.models import (
generate_position_secret, generate_secret,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
from pretix.base.models.tax import EU_COUNTRIES
from pretix.base.services.export import export
from pretix.base.services.invoices import (
@@ -183,7 +184,7 @@ class OrderDetail(OrderView):
'positions': positions,
'raw': cartpos,
'total': self.object.total,
'payment_fee': self.object.payment_fee,
'fees': self.object.fees.all(),
'net_total': self.object.net_total,
'tax_total': self.object.tax_total,
}
@@ -860,7 +861,7 @@ class OverView(EventPermissionRequiredMixin, TemplateView):
ctx['items_by_category'], ctx['total'] = order_overview(self.request.event, subevent=subevent)
ctx['subevent_warning'] = self.request.event.has_subevents and subevent and (
self.request.event.orders.filter(payment_fee__gt=0).exists()
OrderFee.objects.filter(order__event=self.request.event).exclude(value=0).exists()
)
return ctx
+21 -10
View File
@@ -14,6 +14,7 @@ from django.utils.translation import pgettext, pgettext_lazy, ugettext as _
from pretix.base.exporter import BaseExporter
from pretix.base.models import Order, OrderPosition
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import OrderFee
from pretix.base.services.stats import order_overview
@@ -299,9 +300,10 @@ class OrderTaxListReport(Report):
tz = pytz.timezone(self.event.settings.timezone)
tax_rates = set(
self.event.orders.exclude(payment_fee=0).values_list('payment_fee_tax_rate', flat=True)
.filter(status__in=self.form_data['status'])
.distinct().order_by()
a for a
in OrderFee.objects.filter(
order__event=self.event
).values_list('tax_rate', flat=True).distinct().order_by()
)
tax_rates |= set(
a for a
@@ -347,13 +349,20 @@ class OrderTaxListReport(Report):
order__status__in=self.form_data['status'],
order__event=self.event,
).values(
'order__code', 'order__datetime', 'order__payment_date', 'order__total', 'order__payment_fee',
'order__payment_fee_tax_rate', 'order__payment_fee_tax_value', 'tax_rate', 'order__status'
'order__code', 'order__datetime', 'order__payment_date', 'order__total', 'tax_rate', 'order__status',
'order__id'
).annotate(prices=Sum('price'), tax_values=Sum('tax_value')).order_by(
'order__datetime' if self.form_data['sort'] == 'datetime' else 'order__payment_date',
'order__datetime',
'order__code'
)
fee_sum_cache = {
(o['order__id'], o['tax_rate']): o for o in
OrderFee.objects.values('tax_rate', 'order__id').order_by().annotate(
taxsum=Sum('tax_value'), grosssum=Sum('value')
)
}
last_order_code = None
tax_sums = defaultdict(Decimal)
price_sums = defaultdict(Decimal)
@@ -370,11 +379,13 @@ class OrderTaxListReport(Report):
] + sum((['', ''] for t in tax_rates), []),
)
last_order_code = op['order__code']
if op['order__payment_fee_tax_value']:
tdata[-1][5 + 2 * tax_rates.index(op['order__payment_fee_tax_rate'])] = str(op['order__payment_fee'])
tdata[-1][6 + 2 * tax_rates.index(op['order__payment_fee_tax_rate'])] = str(op['order__payment_fee_tax_value'])
tax_sums[op['order__payment_fee_tax_rate']] += op['order__payment_fee_tax_value']
price_sums[op['order__payment_fee_tax_rate']] += op['order__payment_fee']
for i, rate in enumerate(tax_rates):
odata = fee_sum_cache.get((op['order__id'], rate))
if odata:
tdata[-1][5 + 2 * i] = str(odata['grosssum'] or '0.00')
tdata[-1][6 + 2 * i] = str(odata['taxsum'] or '0.00')
tax_sums[rate] += odata['taxsum'] or 0
price_sums[rate] += odata['grosssum'] or 0
i = tax_rates.index(op['tax_rate'])
tdata[-1][5 + 2 * i] = str(Decimal(tdata[-1][5 + 2 * i] or '0') + op['prices'])
@@ -156,28 +156,28 @@
<div class="clearfix"></div>
</div>
{% endfor %}
{% if cart.payment_fee %}
{% for fee in cart.fees %}
<div class="row cart-row">
<div class="col-md-4 col-xs-6">
<strong>{% trans "Payment method fee" %}</strong>
</div>
<div class="col-md-3 col-xs-6 col-md-offset-5 price">
{% if event.settings.display_net_prices %}
<strong>{{ event.currency }} {{ cart.payment_fee_net|floatformat:2 }}</strong>
{% if cart.payment_fee_tax_rate %}
<strong>{{ event.currency }} {{ fee.net_value|floatformat:2 }}</strong>
{% if fee.tax_rate %}
<br />
<small>
{% blocktrans trimmed with rate=cart.payment_fee_tax_rate taxname=cart.payment_fee_tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate taxname=fee.tax_rule.name|default:s_taxes %}
<strong>plus</strong> {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
{% endif %}
{% else %}
<strong>{{ event.currency }} {{ cart.payment_fee|floatformat:2 }}</strong>
{% if cart.payment_fee_tax_rate %}
<strong>{{ event.currency }} {{ fee.value|floatformat:2 }}</strong>
{% if fee.tax_rate %}
<br />
<small>
{% blocktrans trimmed with rate=cart.payment_fee_tax_rate taxname=cart.payment_fee_tax_rule.name|default:s_taxes %}
{% blocktrans trimmed with rate=fee.tax_rate taxname=fee.tax_rule.name|default:s_taxes %}
incl. {{ rate }}% {{ taxname }}
{% endblocktrans %}
</small>
@@ -186,7 +186,7 @@
</div>
<div class="clearfix"></div>
</div>
{% endif %}
{% endfor %}
{% if event.settings.display_net_prices and cart.tax_total %}
<div class="row cart-row total">
<div class="col-md-4 col-xs-6">
+10 -37
View File
@@ -1,6 +1,5 @@
from collections import defaultdict
from datetime import timedelta
from decimal import Decimal
from itertools import groupby
from django.db.models import Sum
@@ -8,7 +7,7 @@ from django.utils.functional import cached_property
from django.utils.timezone import now
from pretix.base.models import CartPosition, InvoiceAddress, OrderPosition
from pretix.base.models.tax import TaxRule
from pretix.base.services.cart import get_fees
from pretix.presale.signals import question_form_fields
@@ -101,34 +100,21 @@ class CartMixin:
tax_total = sum(p.total - p.net_total for p in positions)
if order:
payment_fee = order.payment_fee
tax_total += order.payment_fee_tax_value
payment_fee_net = order.payment_fee - order.payment_fee_tax_value
net_total += payment_fee_net
payment_fee_tax_rule = order.payment_fee_tax_rule
payment_fee_tax_rate = order.payment_fee_tax_rate
fees = order.fees.all()
else:
payment_fee = self.get_payment_fee(total)
payment_fee_tax_rule = self.request.event.settings.tax_rate_default or TaxRule.zero()
iapk = self.request.session.get('invoice_address_{}'.format(self.request.event.pk))
ia = None
if payment_fee_tax_rule.eu_reverse_charge and iapk:
if iapk:
try:
ia = InvoiceAddress.objects.get(pk=iapk, order__isnull=True)
except InvoiceAddress.DoesNotExist:
pass
if payment_fee_tax_rule.tax_applicable(ia):
payment_fee_tax = payment_fee_tax_rule.tax(payment_fee, base_price_is='gross')
tax_total += payment_fee_tax.tax
net_total += payment_fee_tax.net
payment_fee_net = payment_fee_tax.net
payment_fee_tax_rate = payment_fee_tax.rate
else:
net_total += payment_fee
payment_fee_net = payment_fee
payment_fee_tax_rate = Decimal('0.00')
fees = get_fees(self.request.event, total, ia, self.request.session.get('payment'))
total += sum([f.value for f in fees])
net_total += sum([f.net_value for f in fees])
tax_total += sum([f.tax_value for f in fees])
try:
first_expiry = min(p.expires for p in positions) if positions else now()
@@ -140,28 +126,15 @@ class CartMixin:
return {
'positions': positions,
'raw': cartpos,
'total': total + payment_fee,
'total': total,
'net_total': net_total,
'tax_total': tax_total,
'payment_fee': payment_fee,
'payment_fee_net': payment_fee_net,
'payment_fee_tax_rate': payment_fee_tax_rate,
'payment_fee_tax_rule': payment_fee_tax_rule,
'fees': fees,
'answers': answers,
'minutes_left': minutes_left,
'first_expiry': first_expiry,
}
def get_payment_fee(self, total):
if total == 0:
return Decimal('0.00')
payment_fee = 0
if 'payment' in self.request.session:
provider = self.request.event.get_payment_providers().get(self.request.session['payment'])
if provider:
payment_fee = provider.calculate_fee(total)
return payment_fee
def get_cart(request):
if not hasattr(request, '_cart_cache'):
+22 -7
View File
@@ -1,5 +1,6 @@
import mimetypes
import os
from decimal import Decimal
from django.contrib import messages
from django.db import transaction
@@ -12,7 +13,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.generic import TemplateView, View
from pretix.base.models import CachedTicket, Invoice, Order, OrderPosition
from pretix.base.models.orders import InvoiceAddress, QuestionAnswer
from pretix.base.models.orders import InvoiceAddress, OrderFee, QuestionAnswer
from pretix.base.payment import PaymentException
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
@@ -305,11 +306,12 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
if not provider.is_enabled or not provider.order_change_allowed(self.order):
continue
fee = provider.calculate_fee(self._total_order_value)
current_fee = self.order.fees.filter(fee_type=OrderFee.FEE_TYPE_PAYMENT).aggregate(s=Sum('value'))['s'] or Decimal('0.00')
providers.append({
'provider': provider,
'fee': fee,
'fee_diff': fee - self.order.payment_fee,
'fee_diff_abs': abs(fee - self.order.payment_fee),
'fee_diff': fee - current_fee,
'fee_diff_abs': abs(fee - current_fee),
'total': abs(self._total_order_value + fee),
'form': provider.payment_form_render(self.request)
})
@@ -323,16 +325,29 @@ class OrderPayChangeMethod(EventViewMixin, OrderDetailMixin, TemplateView):
request.session['payment_change_{}'.format(self.order.pk)] = '1'
new_fee = p['provider'].calculate_fee(self._total_order_value)
if new_fee:
fee = self.order.fees.get_or_create(fee_type=OrderFee.FEE_TYPE_PAYMENT, defaults={'value': 0})[0]
old_fee = fee.value
fee.value = new_fee
fee.internal_type = p['provider'].identifier
fee._calculate_tax()
fee.save()
else:
try:
fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
old_fee = fee.value
fee.delete()
except OrderFee.DoesNotExist:
old_fee = Decimal('0.00')
self.order.payment_provider = p['provider'].identifier
self.order.payment_fee = new_fee
self.order.total = self._total_order_value + new_fee
self.order._calculate_tax()
self.order.total = self._total_order_value + (self.order.fees.aggregate(sum=Sum('value'))['sum'] or 0)
resp = p['provider'].order_prepare(request, self.order)
if resp:
with transaction.atomic():
self.order.log_action('pretix.event.order.payment.changed', {
'old_fee': self.order.payment_fee,
'old_fee': old_fee,
'new_fee': new_fee,
'old_provider': self.order.payment_provider,
'new_provider': p['provider'].identifier
+39 -7
View File
@@ -1,12 +1,15 @@
import datetime
from decimal import Decimal
from distutils.version import LooseVersion
from unittest import mock
import pytest
from django_countries.fields import Country
from pytz import UTC
from pretix import __version__
from pretix.base.models import InvoiceAddress, Order, OrderPosition
from pretix.base.models.orders import OrderFee
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice,
)
@@ -18,7 +21,12 @@ def item(event):
@pytest.fixture
def order(event, item):
def taxrule(event):
return event.tax_rules.create(rate=Decimal('19.00'))
@pytest.fixture
def order(event, item, taxrule):
testtime = datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=UTC)
with mock.patch('django.utils.timezone.now') as mock_now:
@@ -30,6 +38,8 @@ def order(event, item):
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=UTC),
total=23, payment_provider='banktransfer', locale='en'
)
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
tax_value=Decimal('0.05'), tax_rule=taxrule)
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
OrderPosition.objects.create(
order=o,
@@ -71,11 +81,20 @@ TEST_ORDER_RES = {
"datetime": "2017-12-01T10:00:00Z",
"expires": "2017-12-10T10:00:00Z",
"payment_date": None,
"fees": [
{
"fee_type": "payment",
"value": "0.25",
"description": "",
"internal_type": "",
"tax_rate": "19.00",
"tax_value": "0.05"
}
],
"payment_provider": "banktransfer",
"payment_fee": "0.00",
"payment_fee_tax_rate": "0.00",
"payment_fee_tax_value": "0.00",
"payment_fee_tax_rule": None,
"payment_fee": "0.25",
"payment_fee_tax_rate": "19.00",
"payment_fee_tax_value": "0.05",
"total": "23.00",
"comment": "",
"invoice_address": {
@@ -96,10 +115,15 @@ TEST_ORDER_RES = {
@pytest.mark.django_db
def test_order_list(token_client, organizer, event, order, item):
@pytest.mark.xfail(
LooseVersion(__version__) >= LooseVersion("1.9.0.dev0"),
reason="Deprecated attributes payment_fee_* should be removed by now",
)
def test_order_list(token_client, organizer, event, order, item, taxrule):
res = dict(TEST_ORDER_RES)
res["positions"][0]["id"] = order.positions.first().pk
res["positions"][0]["item"] = item.pk
res["fees"][0]["tax_rule"] = taxrule.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/'.format(organizer.slug, event.slug))
assert resp.status_code == 200
@@ -129,10 +153,11 @@ def test_order_list(token_client, organizer, event, order, item):
@pytest.mark.django_db
def test_order_detail(token_client, organizer, event, order, item):
def test_order_detail(token_client, organizer, event, order, item, taxrule):
res = dict(TEST_ORDER_RES)
res["positions"][0]["id"] = order.positions.first().pk
res["positions"][0]["item"] = item.pk
res["fees"][0]["tax_rule"] = taxrule.pk
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/'.format(organizer.slug, event.slug,
order.code))
assert resp.status_code == 200
@@ -282,6 +307,13 @@ TEST_INVOICE_RES = {
"tax_value": "0.00",
"tax_name": "",
"tax_rate": "0.00"
},
{
"description": "Payment fee",
"gross_value": "0.25",
"tax_value": "0.05",
"tax_name": "",
"tax_rate": "19.00"
}
]
}
+15 -11
View File
@@ -12,6 +12,7 @@ from pretix.base.models import (
Event, Invoice, InvoiceAddress, Item, ItemVariation, Order, OrderPosition,
Organizer,
)
from pretix.base.models.orders import OrderFee
from pretix.base.services.invoices import (
build_preview_invoice_pdf, generate_cancellation, generate_invoice,
invoice_pdf_task, regenerate_invoice,
@@ -27,15 +28,15 @@ def env():
organizer=o, name='Dummy', slug='dummy',
date_from=now(), plugins='pretix.plugins.banktransfer'
)
tr = event.tax_rules.create(rate=Decimal('19.00'))
o = Order.objects.create(
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer',
payment_fee=Decimal('0.25'), payment_fee_tax_rate=Decimal('19.00'),
payment_fee_tax_value=Decimal('0.04'), locale='en', payment_fee_tax_rule=tr
total=0, payment_provider='banktransfer', locale='en'
)
tr = event.tax_rules.create(rate=Decimal('19.00'))
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
tax_value=Decimal('0.05'), tax_rule=tr)
ticket = Item.objects.create(event=event, name='Early-bird ticket',
category=None, default_price=23, tax_rule=tr,
admission=True)
@@ -206,9 +207,10 @@ def test_positions(env):
last = inv.lines.last()
assert 'Payment' in last.description
assert last.gross_value == order.payment_fee
assert last.tax_rate == order.payment_fee_tax_rate
assert last.tax_value == order.payment_fee_tax_value
fee = order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
assert last.gross_value == fee.value
assert last.tax_rate == fee.tax_rate
assert last.tax_value == fee.tax_value
assert inv.invoice_to == ""
@@ -274,9 +276,10 @@ def test_invoice_numbers(env):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer',
payment_fee=Decimal('0.25'), payment_fee_tax_rate=0,
payment_fee_tax_value=0, locale='en'
locale='en'
)
order2.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'))
inv1 = generate_invoice(order)
inv2 = generate_invoice(order)
@@ -320,9 +323,10 @@ def test_invoice_number_prefixes(env):
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer',
payment_fee=Decimal('0.25'), payment_fee_tax_rate=0,
payment_fee_tax_value=0, locale='en'
locale='en'
)
order2.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('0.00'),
tax_value=Decimal('0.00'))
event.settings.set('invoice_numbers_consecutive', False)
event2.settings.set('invoice_numbers_consecutive', False)
assert generate_invoice(order).number == 'DUMMY-{}-1'.format(order.code)
+17 -13
View File
@@ -13,6 +13,7 @@ from pretix.base.models import (
CartPosition, Event, InvoiceAddress, Item, Order, OrderPosition, Organizer,
)
from pretix.base.models.items import SubEventItem
from pretix.base.models.orders import OrderFee
from pretix.base.payment import FreeOrderProvider
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.invoices import generate_invoice
@@ -539,10 +540,10 @@ class OrderChangeManagerTests(TestCase):
self.ocm.commit()
self.order.refresh_from_db()
assert self.order.total == Decimal('47.30')
assert self.order.payment_fee == prov.calculate_fee(self.order.total)
assert self.order.payment_fee_tax_rate == Decimal('19.00')
assert round_decimal(self.order.payment_fee * (
1 - 100 / (100 + self.order.payment_fee_tax_rate))) == self.order.payment_fee_tax_value
fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
assert fee.value == prov.calculate_fee(self.order.total)
assert fee.tax_rate == Decimal('19.00')
assert round_decimal(fee.value * (1 - 100 / (100 + fee.tax_rate))) == fee.tax_value
def test_require_pending(self):
self.order.status = Order.STATUS_PAID
@@ -725,9 +726,10 @@ class OrderChangeManagerTests(TestCase):
self.ocm._recalculate_total_and_payment_fee()
assert self.order.total == Decimal('46.30')
assert self.order.payment_fee == prov.calculate_fee(self.order.total)
assert self.order.payment_fee_tax_rate == Decimal('19.00')
assert self.order.payment_fee_tax_value == Decimal('0.05')
fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
assert fee.value == prov.calculate_fee(self.order.total)
assert fee.tax_rate == Decimal('19.00')
assert fee.tax_value == Decimal('0.05')
ia = self._enable_reverse_charge()
self.ocm.recalculate_taxes()
@@ -739,9 +741,10 @@ class OrderChangeManagerTests(TestCase):
assert op.tax_rate == Decimal('0.00')
assert self.order.total == Decimal('43.30')
assert self.order.payment_fee == prov.calculate_fee(self.order.total)
assert self.order.payment_fee_tax_rate == Decimal('0.00')
assert self.order.payment_fee_tax_value == Decimal('0.00')
fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
assert fee.value == prov.calculate_fee(self.order.total)
assert fee.tax_rate == Decimal('0.00')
assert fee.tax_value == Decimal('0.00')
ia.vat_id_validated = False
ia.save()
@@ -755,6 +758,7 @@ class OrderChangeManagerTests(TestCase):
assert op.tax_rate == Decimal('7.00')
assert self.order.total == Decimal('46.32')
assert self.order.payment_fee == prov.calculate_fee(self.order.total)
assert self.order.payment_fee_tax_rate == Decimal('19.00')
assert self.order.payment_fee_tax_value == Decimal('0.05')
fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
assert fee.value == prov.calculate_fee(self.order.total)
assert fee.tax_rate == Decimal('19.00')
assert fee.tax_value == Decimal('0.05')
+5 -6
View File
@@ -5,6 +5,7 @@ from django.utils.timezone import now
from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, Order, Organizer, Team, User
from pretix.base.models.orders import OrderFee
class TaxRateFormTest(SoupTest):
@@ -76,18 +77,16 @@ class TaxRateFormTest(SoupTest):
self.assertIn("VAT", doc.select("#page-wrapper")[0].text)
assert self.event1.tax_rules.exists()
def test_delete_order_existing(self):
def test_delete_fee_existing(self):
tr = self.event1.tax_rules.create(rate=19, name="VAT")
self.event1.orders.create(
o = self.event1.orders.create(
code='FOO', event=self.event1, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=14, payment_provider='banktransfer', locale='en',
payment_fee=0.3,
payment_fee_tax_rate=19,
payment_fee_tax_value=0.05,
payment_fee_tax_rule=tr
)
o.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('19.00'),
tax_value=Decimal('0.05'), tax_rule=tr)
doc = self.get_doc('/control/event/%s/%s/settings/tax/%s/delete' % (self.orga1.slug, self.event1.slug, tr.id))
form_data = extract_form_fields(doc.select('.container-fluid form')[0])
doc = self.post_doc('/control/event/%s/%s/settings/tax/%s/delete' % (self.orga1.slug, self.event1.slug, tr.id),
+4 -2
View File
@@ -11,6 +11,7 @@ from pretix.base.models import (
Event, Item, ItemCategory, ItemVariation, Order, OrderPosition, Organizer,
Question, Quota,
)
from pretix.base.models.orders import OrderFee
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.invoices import generate_invoice
@@ -417,8 +418,9 @@ class OrdersTest(TestCase):
)
self.order.refresh_from_db()
assert self.order.payment_provider == 'testdummy'
assert self.order.payment_fee == Decimal('12.00')
assert self.order.total == Decimal('23.00') + self.order.payment_fee
fee = self.order.fees.get(fee_type=OrderFee.FEE_TYPE_PAYMENT)
assert fee.value == Decimal('12.00')
assert self.order.total == Decimal('23.00') + fee.value
assert self.order.invoices.count() == 3
def test_answer_download_token(self):