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