Pluggable invoice transmission methods (#5020)

* Flexible invoice transmission

* UI work

* Add peppol and output

* API support

* Profile integration

* Simplify form for individuals

* Remove sent_to_customer usage

* more steps

* Revert "Bank transfer: Allow to send the invoice direclty to the accounting department (#2975)"

This reverts commit cea6c340be.

* minor fixes

* Fixes after rebase

* update stati

* Backend view

* Transmit and show status

* status, retransmission

* API retransmission

* More fields

* API docs

* Plugin docs

* Update migration

* Add missing license headers

* Remove dead code, fix current tests

* Run isort

* Update regex

* Rebase migration

* Fix migration

* Add tests, fix bugs

* Rebase migration

* Apply suggestion from @luelista

Co-authored-by: luelista <weller@rami.io>

* Apply suggestion from @luelista

Co-authored-by: luelista <weller@rami.io>

* Apply suggestion from @luelista

Co-authored-by: luelista <weller@rami.io>

* Apply suggestion from @luelista

Co-authored-by: luelista <weller@rami.io>

* Apply suggestion from @luelista

Co-authored-by: luelista <weller@rami.io>

* Make migration reversible

* Add TransmissionType.enforce_transmission

* Fix registries API usage after rebase

* Remove code I forgot to delete

* Update transmission status display depending on type

* Add testmode_supported

* Update src/pretix/static/pretixbase/js/addressform.js

Co-authored-by: luelista <weller@rami.io>

* Update src/pretix/static/pretixbase/js/addressform.js

Co-authored-by: luelista <weller@rami.io>

* Update src/pretix/static/pretixbase/js/addressform.js

Co-authored-by: luelista <weller@rami.io>

* New mechanism for non-required invoice forms

* Update src/pretix/base/invoicing/transmission.py

Co-authored-by: luelista <weller@rami.io>

* Declare testmode_supported for email

* Make transmission_email_other an implementation detail

* Fix failing tests and add new ones

* Update src/pretix/base/services/invoices.py

Co-authored-by: luelista <weller@rami.io>

* Add emails to email history

* Fix comma error

* More generic default email text

* Cleanup

* Remove "email invoices" button and refine logic

* Rebase migration

* Fix edge case

---------

Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
Raphael Michel
2025-08-19 17:59:45 +02:00
committed by GitHub
parent 37910f6037
commit 05c74b7ad6
65 changed files with 4514 additions and 1825 deletions

View File

@@ -60,6 +60,12 @@ from pretix.testutils.sessions import get_cart_session_key
from .test_timemachine import TimemachineTestMixin
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class BaseCheckoutTestCase:
@scopes_disabled()
def setUp(self):
@@ -98,6 +104,7 @@ class BaseCheckoutTestCase:
self.workshopquota.items.add(self.workshop2)
self.workshopquota.variations.add(self.workshop2a)
self.workshopquota.variations.add(self.workshop2b)
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
def _set_session(self, key, value):
session = self.client.session
@@ -169,7 +176,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
assert b'TRACKING SCRIPT' not in payment_r.content
@@ -201,7 +209,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -224,7 +233,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
with scopes_disabled():
@@ -260,7 +270,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
assert 'alert-danger' in resp.content.decode()
@@ -291,7 +302,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'country': 'AU',
'state': 'QLD',
'vat_id': 'AU123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -324,7 +336,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -355,7 +368,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'FR',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
assert 'alert-danger' in resp.content.decode()
@@ -388,7 +402,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -422,7 +437,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -459,7 +475,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -476,7 +493,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -508,7 +526,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '12345',
'city': 'Here',
'country': 'DE',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
with mock.patch('pretix.base.services.tax._validate_vat_id_EU') as mock_validate:
@@ -522,7 +541,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
@@ -556,7 +576,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '12345',
'city': 'Here',
'country': 'DE',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(r.content.decode(), "lxml")
assert doc.select(".alert-danger")
@@ -575,7 +596,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(r.content.decode(), "lxml")
assert not doc.select(".alert-danger")
@@ -605,7 +627,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '12345',
'city': 'Here',
'country': 'DE',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
@@ -633,7 +656,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -685,7 +709,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '1234',
'city': 'Here',
'country': 'AT',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -714,7 +739,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'US',
'state': 'CA',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
with scopes_disabled():
@@ -767,7 +793,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -831,7 +858,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'AT',
'vat_id': 'AT123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
cr1.refresh_from_db()
@@ -868,7 +896,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
f = SimpleUploadedFile("testfile.txt", b"file_content")
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-question_%s' % (cr1.id, q1.id): f,
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -883,7 +912,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Delete
self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-question_%s-clear' % (cr1.id, q1.id): 'on',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
with scopes_disabled():
assert not cr1.answers.exists()
@@ -904,6 +934,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -913,6 +944,7 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'email': 'admin@localhost',
'phone_0': '+49',
'phone_1': '0622199999', # yeah the 0 is wrong but users don't know that so it should work fine
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -942,7 +974,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-attendee_email' % cr1.id: '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -950,7 +983,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-attendee_email' % cr1.id: 'foo@localhost',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -974,7 +1008,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-company' % cr1.id: '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -982,7 +1017,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-company' % cr1.id: 'foobar',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1022,7 +1058,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'%s-city' % cr1.id: 'Musterstadt',
'%s-country' % cr1.id: 'DE',
'%s-state' % cr1.id: '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1049,7 +1086,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-attendee_name_parts_0' % cr1.id: '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -1057,7 +1095,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-attendee_name_parts_0' % cr1.id: 'Peter',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1082,7 +1121,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Accepted request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1112,7 +1152,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'%s-attendee_name_parts_1' % cr1.id: '',
'%s-attendee_name_parts_2' % cr1.id: 'John',
'%s-attendee_name_parts_3' % cr1.id: 'Doe',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
})
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1143,7 +1184,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Not all fields filled out, expect success
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-attendee_name_parts_0' % cr1.id: '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1173,7 +1215,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -1191,7 +1234,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1233,7 +1277,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '',
'city': '',
'country': 'BI',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -1250,7 +1295,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'zipcode': '',
'city': 'Bujumbura',
'country': 'BI',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1293,7 +1339,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -1311,11 +1358,168 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
def test_invoice_transmission_providers_email_special_case(self):
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_generate = "True"
self.event.settings.invoice_email_attachment = True
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="transmission_email_other"]')), 1)
self.event.settings.invoice_generate = "False"
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="transmission_email_other"]')), 0)
def test_invoice_transmission_providers_all_fields_loaded(self):
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_generate = "True"
self.event.settings.invoice_email_attachment = True
response = self.client.get('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="transmission_email_address"]')), 1)
self.assertEqual(len(doc.select('input[name="transmission_email_other"]')), 1)
self.assertEqual(len(doc.select('input[name="transmission_it_sdi_codice_fiscale"]')), 1)
self.assertEqual(len(doc.select('input[name="transmission_it_sdi_pec"]')), 1)
self.assertEqual(len(doc.select('input[name="transmission_it_sdi_recipient_code"]')), 1)
self.assertEqual(len(doc.select('input[name="transmission_peppol_participant_id"]')), 1)
self.assertEqual(len(doc.select('select[name="transmission_type"]')), 1)
self.assertEqual(len(doc.select('select[name="transmission_type"] option')), 3)
def test_invoice_transmission_invalid_submission(self):
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_generate = "True"
self.event.settings.invoice_email_attachment = True
self.event.settings.invoice_address_vatid = True
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'Baz',
'zipcode': '12345',
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost',
'transmission_type': 'it_sdi',
}, follow=True)
assert "The selected transmission type is not available" in response.content.decode()
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'Baz',
'zipcode': '12345',
'city': 'Here',
'country': 'IT',
'state': 'MI',
'email': 'admin@localhost',
'transmission_type': 'it_sdi',
'vat_id': '',
}, follow=True)
assert "This field is required for the selected type" in response.content.decode()
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'Baz',
'zipcode': '12345',
'city': 'Here',
'country': 'IT',
'state': 'MI',
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
assert "must be used for this country" in response.content.decode()
def test_invoice_optional_transmission(self):
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = False
self.event.settings.invoice_generate = "True"
self.event.settings.invoice_email_attachment = True
self.event.settings.invoice_address_vatid = True
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': 'Foo',
'name_parts_0': 'Mr',
'name_parts_1': 'John',
'name_parts_2': '',
'name_parts_3': 'Kennedy',
'street': 'Baz',
'zipcode': '12345',
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost',
'transmission_type': '-',
}, follow=True)
assert "If you enter an invoice address, you also need to select an invoice" in response.content.decode()
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'is_business': 'business',
'company': '',
'name_parts_0': '',
'name_parts_1': '',
'name_parts_2': '',
'name_parts_3': '',
'street': '',
'zipcode': '',
'city': '',
'country': 'IT',
'state': '',
'email': 'admin@localhost',
'transmission_type': '-',
'vat_id': '',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
with scopes_disabled():
ia = InvoiceAddress.objects.get()
assert ia.transmission_type == "email"
def test_invoice_address_hidden_for_free(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
@@ -1332,7 +1536,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
self.assertEqual(len(doc.select('input[name="city"]')), 0)
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1365,7 +1570,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'city': 'Here',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1406,7 +1612,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'is_business': 'business',
'country': 'DE',
'city': 'Musterstadt',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -1416,7 +1623,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
'is_business': 'business',
'country': 'DE',
'vat_id': 'DE123456',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -1437,7 +1645,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Not all required fields filled out, expect failure
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -1445,7 +1654,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'name_parts_0': 'Raphael',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -2603,7 +2813,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Date too far in the future, expected to fail
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-requested_valid_from' % cr1.id: '2024-01-20',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -2611,7 +2822,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-requested_valid_from' % cr1.id: '2023-01-20',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -2649,7 +2861,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-requested_valid_from_0' % cr1.id: '2024-01-20',
'%s-requested_valid_from_1' % cr1.id: '11:00:00',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -2658,7 +2871,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-requested_valid_from_0' % cr1.id: '2023-01-20',
'%s-requested_valid_from_1' % cr1.id: '11:00:00',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -2701,7 +2915,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Date too far in the future, expected to fail
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-requested_valid_from' % cr1.id: '2024-01-17',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -2709,7 +2924,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
# Corrected request
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'%s-requested_valid_from' % cr1.id: '2023-01-10',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -2748,7 +2964,8 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
)
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -3797,6 +4014,120 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
o.payments.first().confirm()
assert len(djmail.outbox) == 0
def test_order_confirmation_mail_invoice_attached(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_generate = "True"
self.event.settings.invoice_email_attachment = True
ia = InvoiceAddress.objects.create(
is_business=True,
country=Country('AT'),
transmission_type="email",
transmission_info={
"transmission_email_other": False,
"transmission_email_address": None,
}
)
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
djmail.outbox = []
_perform_order(self.event, self._manual_payment(), [cp1.pk], 'admin@example.org', 'en', ia.pk, {}, 'web')
assert len(djmail.outbox) == 1
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
def test_order_confirmation_mail_invoice_sent_somewhere_else(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_generate = "True"
self.event.settings.invoice_email_attachment = True
ia = InvoiceAddress.objects.create(
is_business=True,
country=Country('AT'),
transmission_type="email",
transmission_info={
"transmission_email_other": True,
"transmission_email_address": "invoice@example.org",
}
)
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
djmail.outbox = []
_perform_order(self.event, self._manual_payment(), [cp1.pk], 'admin@example.org', 'en', ia.pk, {}, 'web')
assert len(djmail.outbox) == 2
assert ["invoice@example.org"] == djmail.outbox[0].to
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
assert ["admin@example.org"] == djmail.outbox[1].to
assert not any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
def test_order_paid_confirmation_mail_invoice_attached(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_generate = "paid"
self.event.settings.invoice_email_attachment = True
ia = InvoiceAddress.objects.create(
is_business=True,
country=Country('AT'),
transmission_type="email",
transmission_info={
"transmission_email_other": False,
"transmission_email_address": None,
}
)
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
djmail.outbox = []
oid = _perform_order(self.event, self._manual_payment(), [cp1.pk], 'admin@example.org', 'en', ia.pk, {}, 'web')
assert len(djmail.outbox) == 1
assert not any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
o = Order.objects.get(pk=oid['order_id'])
o.payments.first().confirm()
assert len(djmail.outbox) == 2
assert any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
def test_order_paid_confirmation_mail_invoice_sent_somewhere_else(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
self.event.settings.invoice_generate = "paid"
self.event.settings.invoice_email_attachment = True
ia = InvoiceAddress.objects.create(
is_business=True,
country=Country('AT'),
transmission_type="email",
transmission_info={
"transmission_email_other": True,
"transmission_email_address": "invoice@example.org",
}
)
with scopes_disabled():
cp1 = CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
djmail.outbox = []
oid = _perform_order(self.event, self._manual_payment(), [cp1.pk], 'admin@example.org', 'en', ia.pk, {}, 'web')
assert len(djmail.outbox) == 1
assert ["admin@example.org"] == djmail.outbox[0].to
assert not any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
o = Order.objects.get(pk=oid['order_id'])
o.payments.first().confirm()
assert len(djmail.outbox) == 3
assert ["invoice@example.org"] == djmail.outbox[2].to
assert any(["Invoice_" in a[0] for a in djmail.outbox[2].attachments])
def test_locale_region_not_saved(self):
self.event.settings.origin = 'US'
self.event.settings.locales = ['de']
@@ -3882,6 +4213,7 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
'%s-question_%s_0' % (cr.id, q3.id): '2018-01-01',
'%s-question_%s_1' % (cr.id, q3.id): '5:23',
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), target_status_code=200)
self.event.settings.set('timezone', 'US/Central')
@@ -3966,7 +4298,8 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
'%s-question_%s' % (cr2.id, q1.id): '',
'%s-question_%s' % (cr1.id, q2.id): 'Internet',
'%s-question_%s' % (cr2.id, q2.id): '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -3977,7 +4310,8 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
'%s-question_%s' % (cr2.id, q1.id): '0',
'%s-question_%s' % (cr1.id, q2.id): 'Internet',
'%s-question_%s' % (cr2.id, q2.id): '',
'email': 'admin@localhost'
'email': 'admin@localhost',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -4000,6 +4334,7 @@ class QuestionsTestCase(BaseCheckoutTestCase, TestCase):
('%s-question_%s' % (cr1.id, k.id)): v for k, v in data.items() if v != 'False'
}
pl['email'] = 'admin@localhost'
pl['transmission_type'] = 'email'
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), pl, follow=True)
if should_fail:
doc = BeautifulSoup(response.content.decode(), "lxml")
@@ -4919,7 +5254,8 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
self.assertRedirects(response, '/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug),
target_status_code=200)
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'will-be-ignored'
'email': 'will-be-ignored',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -5003,7 +5339,8 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
target_status_code=200)
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'will-be-ignored'
'email': 'will-be-ignored',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
@@ -5140,6 +5477,7 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
assert b'Mark Fisher' not in response.content
response = self.client.post('/%s/%s/checkout/questions/' % (self.orga.slug, self.event.slug), {
'email': 'will-be-ignored',
'transmission_type': 'email',
f'{cp.pk}-attendee_name_parts_0': 'will-be-ignored'
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),

View File

@@ -273,6 +273,7 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-attendee_name_parts_0' % self.ticket_pos.id: '',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -304,6 +305,7 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-attendee_name_parts_0' % self.ticket_pos.id: 'Peter',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response, '/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.order.secret),
@@ -326,6 +328,7 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): '',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -350,6 +353,7 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): '',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -357,6 +361,7 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): 'ABC',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -374,6 +379,7 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): 'ABC',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -390,6 +396,7 @@ class OrdersTest(BaseOrdersTest):
'street': 'Main Street',
'city': 'Heidelberg',
'country': 'DE',
'transmission_type': 'email',
}, follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertGreaterEqual(len(doc.select('.has-error')), 1)
@@ -403,6 +410,7 @@ class OrdersTest(BaseOrdersTest):
response = self.client.post(
'/%s/%s/order/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code, self.order.secret), {
'%s-question_%s' % (self.ticket_pos.id, self.question.id): 'ABC',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -419,6 +427,7 @@ class OrdersTest(BaseOrdersTest):
'street': 'Main Street',
'city': 'Heidelberg',
'country': 'DE',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -436,6 +445,7 @@ class OrdersTest(BaseOrdersTest):
'street': 'Main Street',
'city': 'Heidelberg',
'country': 'DE',
'transmission_type': 'email',
}, follow=True)
self.assertRedirects(response,
'/%s/%s/order/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
@@ -477,8 +487,12 @@ class OrdersTest(BaseOrdersTest):
# Not all fields filled out, expect success
response = self.client.post(
'/%s/%s/ticket/%s/%s/%s/modify' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret)
self.ticket_pos.positionid, self.ticket_pos.web_secret),
{
'transmission_type': 'email',
}
)
print(response.content.decode())
self.assertRedirects(response,
'/%s/%s/ticket/%s/%s/%s/' % (self.orga.slug, self.event.slug, self.order.code,
self.ticket_pos.positionid, self.ticket_pos.web_secret),