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

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