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

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):

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'
)

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': [
{

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()]))

View File

@@ -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',
),
]

View File

@@ -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')},
),
]

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')

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

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:

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

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):

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))

View File

@@ -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">

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

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

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'])

View File

@@ -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">

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'):

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

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"
}
]
}

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)

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')

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),

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):