Fix #571 -- Partial payments and refunds

This commit is contained in:
Raphael Michel
2018-06-26 12:09:36 +02:00
parent 8e7af49206
commit 18a378976b
115 changed files with 6026 additions and 1598 deletions

View File

@@ -32,7 +32,7 @@ def env():
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer', locale='en'
total=0, 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'),
@@ -275,7 +275,7 @@ def test_invoice_numbers(env):
code='BAR', event=event, email='dummy2@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer',
total=0,
locale='en'
)
order2.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('0.00'),
@@ -322,7 +322,7 @@ def test_invoice_number_prefixes(env):
event=event2, email='dummy2@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer',
total=0,
locale='en'
)
order2.fees.create(fee_type=OrderFee.FEE_TYPE_PAYMENT, value=Decimal('0.25'), tax_rate=Decimal('0.00'),

View File

@@ -16,15 +16,13 @@ from django.utils.timezone import now
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, CartPosition, CheckinList, Event, Item, ItemCategory,
ItemVariation, Order, OrderPosition, Organizer, Question, Quota, User,
Voucher, WaitingListEntry,
ItemVariation, Order, OrderPayment, OrderPosition, OrderRefund, Organizer,
Question, Quota, User, Voucher, WaitingListEntry,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.items import SubEventItem, SubEventItemVariation
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.orders import (
OrderError, cancel_order, mark_order_paid, perform_order,
)
from pretix.base.services.orders import OrderError, cancel_order, perform_order
class UserTestCase(TestCase):
@@ -600,7 +598,9 @@ class OrderTestCase(BaseQuotaTestCase):
def test_paid_in_time(self):
self.quota.size = 0
self.quota.save()
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_PAID)
@@ -609,7 +609,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.order.status = Order.STATUS_EXPIRED
self.order.expires = now() - timedelta(days=2)
self.order.save()
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_PAID)
@@ -619,7 +621,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.order.expires = now() - timedelta(days=2)
self.order.save()
with self.assertRaises(Quota.QuotaExceededException):
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
@@ -640,7 +644,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.order.expires = now() - timedelta(days=2)
self.order.save()
with self.assertRaises(Quota.QuotaExceededException):
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
self.event.has_subevents = False
@@ -652,7 +658,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.order.expires = now() - timedelta(days=2)
self.order.save()
with self.assertRaises(Quota.QuotaExceededException):
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
@@ -664,7 +672,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.quota.size = 0
self.quota.save()
with self.assertRaises(Quota.QuotaExceededException):
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertIn(self.order.status, (Order.STATUS_PENDING, Order.STATUS_EXPIRED))
@@ -672,7 +682,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.event.settings.payment_term_accept_late = True
self.order.expires = now() - timedelta(days=2)
self.order.save()
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_PAID)
@@ -683,7 +695,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.order.save()
self.quota.size = 0
self.quota.save()
mark_order_paid(self.order, force=True)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm(force=True)
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_PAID)
@@ -696,7 +710,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.quota.size = 2
self.quota.save()
with self.assertRaises(Quota.QuotaExceededException):
mark_order_paid(self.order)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm()
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_EXPIRED)
@@ -707,7 +723,9 @@ class OrderTestCase(BaseQuotaTestCase):
self.order.save()
self.quota.size = 2
self.quota.save()
mark_order_paid(self.order, count_waitinglist=False)
self.order.payments.create(
provider='manual', amount=self.order.total
).confirm(count_waitinglist=False)
self.order = Order.objects.get(id=self.order.id)
self.assertEqual(self.order.status, Order.STATUS_PAID)
@@ -866,6 +884,144 @@ class OrderTestCase(BaseQuotaTestCase):
assert p1.secret != p2.secret
assert self.order.can_user_cancel is False
def test_paid_order_underpaid(self):
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.payments.create(
amount=Decimal('46.00'),
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='manual'
)
self.order.refunds.create(
amount=Decimal('10.00'),
state=OrderRefund.REFUND_STATE_DONE,
provider='manual'
)
assert self.order.pending_sum == Decimal('10.00')
o = Order.annotate_overpayments(Order.objects.all()).first()
assert o.is_underpaid
assert not o.is_overpaid
assert not o.has_pending_refund
assert not o.has_external_refund
def test_pending_order_underpaid(self):
self.order.payments.create(
amount=Decimal('46.00'),
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='manual'
)
self.order.refunds.create(
amount=Decimal('10.00'),
state=OrderRefund.REFUND_STATE_DONE,
provider='manual'
)
assert self.order.pending_sum == Decimal('10.00')
o = Order.annotate_overpayments(Order.objects.all()).first()
assert not o.is_underpaid
assert not o.is_overpaid
assert not o.has_pending_refund
assert not o.has_external_refund
def test_canceled_order_overpaid(self):
self.order.status = Order.STATUS_CANCELED
self.order.save()
self.order.payments.create(
amount=Decimal('46.00'),
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='manual'
)
self.order.refunds.create(
amount=Decimal('10.00'),
state=OrderRefund.REFUND_STATE_DONE,
provider='manual'
)
assert self.order.pending_sum == Decimal('-36.00')
o = Order.annotate_overpayments(Order.objects.all()).first()
assert not o.is_underpaid
assert o.is_overpaid
assert not o.has_pending_refund
assert not o.has_external_refund
def test_paid_order_external_refund(self):
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.payments.create(
amount=Decimal('46.00'),
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='manual'
)
self.order.refunds.create(
amount=Decimal('10.00'),
state=OrderRefund.REFUND_STATE_EXTERNAL,
provider='manual'
)
assert self.order.pending_sum == Decimal('0.00')
o = Order.annotate_overpayments(Order.objects.all()).first()
assert not o.is_underpaid
assert not o.is_overpaid
assert not o.has_pending_refund
assert o.has_external_refund
def test_pending_order_pending_refund(self):
self.order.status = Order.STATUS_REFUNDED
self.order.save()
self.order.payments.create(
amount=Decimal('46.00'),
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='manual'
)
self.order.refunds.create(
amount=Decimal('46.00'),
state=OrderRefund.REFUND_STATE_CREATED,
provider='manual'
)
assert self.order.pending_sum == Decimal('0.00')
o = Order.annotate_overpayments(Order.objects.all()).first()
assert not o.is_underpaid
assert not o.is_overpaid
assert o.has_pending_refund
assert not o.has_external_refund
def test_paid_order_overpaid(self):
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.payments.create(
amount=Decimal('66.00'),
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='manual'
)
self.order.refunds.create(
amount=Decimal('10.00'),
state=OrderRefund.REFUND_STATE_DONE,
provider='manual'
)
assert self.order.pending_sum == Decimal('-10.00')
o = Order.annotate_overpayments(Order.objects.all()).first()
assert not o.is_underpaid
assert o.is_overpaid
assert not o.has_pending_refund
assert not o.has_external_refund
def test_pending_order_overpaid(self):
self.order.status = Order.STATUS_PENDING
self.order.save()
self.order.payments.create(
amount=Decimal('56.00'),
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
provider='manual'
)
self.order.refunds.create(
amount=Decimal('10.00'),
state=OrderRefund.REFUND_STATE_DONE,
provider='manual'
)
assert self.order.pending_sum == Decimal('0.00')
o = Order.annotate_overpayments(Order.objects.all()).first()
assert not o.is_underpaid
assert o.is_overpaid
assert not o.has_pending_refund
assert not o.has_external_refund
class ItemCategoryTest(TestCase):
"""
@@ -1130,7 +1286,7 @@ class CheckinListTestCase(TestCase):
code='FOO', event=cls.event, email='dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal("30"), payment_provider='banktransfer', locale='en'
total=Decimal("30"), locale='en'
)
OrderPosition.objects.create(
order=o,
@@ -1157,7 +1313,7 @@ class CheckinListTestCase(TestCase):
code='FOO', event=cls.event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal("30"), payment_provider='banktransfer', locale='en'
total=Decimal("30"), locale='en'
)
op4 = OrderPosition.objects.create(
order=o,

View File

@@ -27,7 +27,7 @@ def order(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('46.00'), payment_provider='banktransfer'
total=Decimal('46.00'),
)
tr19 = event.tax_rules.create(rate=Decimal('19.00'))
ticket = Item.objects.create(event=event, name='Early-bird ticket', tax_rule=tr19,

View File

@@ -13,13 +13,13 @@ 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.models.orders import OrderFee, OrderPayment, OrderRefund
from pretix.base.payment import FreeOrderProvider
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
from pretix.base.services.invoices import generate_invoice
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _create_order, expire_orders,
mark_order_paid, send_download_reminders,
send_download_reminders,
)
@@ -147,13 +147,13 @@ def test_expiring(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer'
total=0,
)
o2 = Order.objects.create(
code='FO2', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
total=12, payment_provider='banktransfer'
total=12,
)
generate_invoice(o2)
expire_orders(None)
@@ -171,14 +171,16 @@ def test_expiring_paid_invoice(event):
code='FO2', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() - timedelta(days=10),
total=12, payment_provider='banktransfer'
total=12,
)
generate_invoice(o2)
expire_orders(None)
o2 = Order.objects.get(id=o2.id)
assert o2.status == Order.STATUS_EXPIRED
assert o2.invoices.count() == 2
mark_order_paid(o2)
o2.payments.create(
provider='manual', amount=o2.total
).confirm()
assert o2.invoices.count() == 3
assert o2.invoices.last().is_cancellation is False
@@ -190,13 +192,13 @@ def test_expiring_auto_disabled(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=0, payment_provider='banktransfer'
total=0,
)
o2 = Order.objects.create(
code='FO2', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() - timedelta(days=10),
total=0, payment_provider='banktransfer'
total=0,
)
expire_orders(None)
o1 = Order.objects.get(id=o1.id)
@@ -219,7 +221,7 @@ class DownloadReminderTests(TestCase):
status=Order.STATUS_PAID, locale='en',
datetime=now(),
expires=now() + timedelta(days=10),
total=Decimal('46.00'), payment_provider='banktransfer'
total=Decimal('46.00'),
)
self.ticket = Item.objects.create(event=self.event, name='Early-bird ticket',
default_price=Decimal('23.00'), admission=True)
@@ -264,7 +266,10 @@ class OrderChangeManagerTests(TestCase):
code='FOO', event=self.event, email='dummy@dummy.test',
status=Order.STATUS_PENDING, locale='en',
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal('46.00'), payment_provider='banktransfer'
total=Decimal('46.00'),
)
self.order.payments.create(
provider='banktransfer', state=OrderPayment.PAYMENT_STATE_CREATED, amount=self.order.total
)
self.tr7 = self.event.tax_rules.create(rate=Decimal('7.00'))
self.tr19 = self.event.tax_rules.create(rate=Decimal('19.00'))
@@ -593,10 +598,9 @@ class OrderChangeManagerTests(TestCase):
self.order.status = Order.STATUS_PAID
self.order.save()
self.ocm.change_item(self.op1, self.shirt, None)
with self.assertRaises(OrderError):
self.ocm.commit()
self.ocm.commit()
self.op1.refresh_from_db()
assert self.op1.item == self.ticket
assert self.op1.item == self.shirt
def test_change_price_to_free_marked_as_paid(self):
self.ocm.change_price(self.op1, Decimal('0.00'))
@@ -605,7 +609,7 @@ class OrderChangeManagerTests(TestCase):
self.order.refresh_from_db()
assert self.order.total == 0
assert self.order.status == Order.STATUS_PAID
assert self.order.payment_provider == 'free'
assert self.order.payments.last().provider == 'free'
def test_change_paid_same_price(self):
self.order.status = Order.STATUS_PAID
@@ -620,12 +624,26 @@ class OrderChangeManagerTests(TestCase):
self.order.status = Order.STATUS_PAID
self.order.save()
self.ocm.change_price(self.op1, Decimal('5.00'))
with self.assertRaises(OrderError):
self.ocm.commit()
self.ocm.commit()
self.order.refresh_from_db()
assert self.order.total == 46
assert self.order.total == Decimal('28.00')
assert self.order.status == Order.STATUS_PAID
def test_change_paid_to_pending(self):
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.payments.create(
provider='manual',
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
amount=self.order.total,
)
self.ocm.change_price(self.op1, Decimal('25.00'))
self.ocm.commit()
self.order.refresh_from_db()
assert self.order.total == Decimal('48.00')
assert self.order.pending_sum == Decimal('2.00')
assert self.order.status == Order.STATUS_PENDING
def test_add_item_quota_required(self):
self.quota.delete()
with self.assertRaises(OrderError):
@@ -775,10 +793,10 @@ class OrderChangeManagerTests(TestCase):
assert fee.tax_rate == Decimal('19.00')
assert fee.tax_value == Decimal('0.05')
self.ocm = OrderChangeManager(self.order, None)
ia = self._enable_reverse_charge()
self.ocm.recalculate_taxes()
self.ocm.commit()
self.ocm = OrderChangeManager(self.order, None)
ops = list(self.order.positions.all())
for op in ops:
assert op.price == Decimal('21.50')
@@ -794,6 +812,7 @@ class OrderChangeManagerTests(TestCase):
ia.vat_id_validated = False
ia.save()
self.ocm = OrderChangeManager(self.order, None)
self.ocm.recalculate_taxes()
self.ocm.commit()
ops = list(self.order.positions.all())
@@ -870,6 +889,11 @@ class OrderChangeManagerTests(TestCase):
def test_split_paid_no_payment_fees(self):
self.order.status = Order.STATUS_PAID
self.order.save()
self.order.payments.create(
provider='manual',
state=OrderPayment.PAYMENT_STATE_CONFIRMED,
amount=self.order.total,
)
# Split
self.ocm.split(self.op2)
@@ -880,6 +904,11 @@ class OrderChangeManagerTests(TestCase):
# First order
assert self.order.total == Decimal('23.00')
assert not self.order.fees.exists()
assert self.order.pending_sum == Decimal('0.00')
r = self.order.refunds.last()
assert r.provider == 'offsetting'
assert r.amount == Decimal('23.00')
assert r.state == OrderRefund.REFUND_STATE_DONE
# New order
assert self.op2.order != self.order
@@ -888,6 +917,11 @@ class OrderChangeManagerTests(TestCase):
assert o2.status == Order.STATUS_PAID
assert o2.positions.count() == 1
assert o2.fees.count() == 0
assert o2.pending_sum == Decimal('0.00')
p = o2.payments.last()
assert p.provider == 'offsetting'
assert p.amount == Decimal('23.00')
assert p.state == OrderPayment.PAYMENT_STATE_CONFIRMED
def test_split_invoice_address(self):
ia = InvoiceAddress.objects.create(
@@ -1026,6 +1060,9 @@ class OrderChangeManagerTests(TestCase):
self.order.status = Order.STATUS_PAID
self.order.save()
payment = self.order.payments.first()
payment.state = OrderPayment.PAYMENT_STATE_CONFIRMED
payment.save()
# Split
self.ocm.split(self.op2)

View File

@@ -150,7 +150,7 @@ def test_availability_date_order_relative_subevents(event):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + datetime.timedelta(days=10),
total=Decimal('46.00'), payment_provider='dummtest'
total=Decimal('46.00'),
)
OrderPosition.objects.create(
order=order, item=ticket, variation=None, subevent=se1,

View File

@@ -9,7 +9,7 @@ from django.utils.timezone import now
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, Order,
OrderPosition, Organizer, QuestionAnswer,
OrderPayment, OrderPosition, Organizer, QuestionAnswer,
)
from pretix.base.services.invoices import generate_invoice, invoice_pdf_task
from pretix.base.services.tickets import generate, generate_order
@@ -45,7 +45,7 @@ def order(event, item):
code='FOO', event=event, email='dummy@dummy.test',
status=Order.STATUS_PENDING,
datetime=now(), expires=now() + timedelta(days=10),
total=14, payment_provider='banktransfer', locale='en'
total=14, locale='en'
)
event.settings.set('attendee_names_asked', True)
event.settings.set('locales', ['en', 'de'])
@@ -295,12 +295,12 @@ def test_cached_tickets(event, order):
@pytest.mark.django_db
def test_payment_info_shredder(event, order):
order.payment_info = json.dumps({
order.payments.create(info=json.dumps({
'reference': 'Verwendungszweck 1',
'date': '2018-05-01',
'payer': 'Hans',
'trans_id': 12
})
}), provider='banktransfer', amount=order.total, state=OrderPayment.PAYMENT_STATE_PENDING)
order.save()
s = PaymentInfoShredder(event)
@@ -308,7 +308,7 @@ def test_payment_info_shredder(event, order):
s.shred_data()
order.refresh_from_db()
assert json.loads(order.payment_info) == {
assert order.payments.first().info_data == {
'_shredded': True,
'reference': '',
'date': '2018-05-01',