From 16983826fbf91e3319cf2e05d112e05e9ab822b3 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Wed, 24 Oct 2018 01:37:18 +0200 Subject: [PATCH] Allow to store structured invoice addresses --- src/pretix/base/invoice.py | 4 +- .../migrations/0100_auto_20181023_2300.py | 79 +++++++++++++++++++ src/pretix/base/models/invoices.py | 27 +++++++ src/pretix/base/services/invoices.py | 20 +++++ src/pretix/control/forms/event.py | 51 +++++++++--- .../pretixcontrol/event/invoicing.html | 31 ++++++-- src/tests/control/test_events.py | 13 ++- 7 files changed, 202 insertions(+), 23 deletions(-) create mode 100644 src/pretix/base/migrations/0100_auto_20181023_2300.py diff --git a/src/pretix/base/invoice.py b/src/pretix/base/invoice.py index f9f93fde65..0f9ffc5edb 100644 --- a/src/pretix/base/invoice.py +++ b/src/pretix/base/invoice.py @@ -223,7 +223,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): p.drawOn(canvas, 25 * mm, (297 - 52) * mm - p_size[1]) def _draw_invoice_from(self, canvas): - p = Paragraph(self.invoice.invoice_from.strip().replace('\n', '
\n'), style=self.stylesheet['Normal']) + p = Paragraph(self.invoice.full_invoice_from.strip().replace('\n', '
\n'), style=self.stylesheet['Normal']) p.wrapOn(canvas, 70 * mm, 50 * mm) p_size = p.wrap(70 * mm, 50 * mm) p.drawOn(canvas, 25 * mm, (297 - 17) * mm - p_size[1]) @@ -330,7 +330,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): return txt if not self.invoice.event.has_subevents: - if self.invoice.event.settings.show_date_to: + if self.invoice.event.settings.show_date_to and self.invoice.event.date_to: p_str = ( shorten(self.invoice.event.name) + '\n' + pgettext('invoice', '{from_date}\nuntil {to_date}').format( from_date=self.invoice.event.get_date_from_display(), diff --git a/src/pretix/base/migrations/0100_auto_20181023_2300.py b/src/pretix/base/migrations/0100_auto_20181023_2300.py new file mode 100644 index 0000000000..c7adf0471b --- /dev/null +++ b/src/pretix/base/migrations/0100_auto_20181023_2300.py @@ -0,0 +1,79 @@ +# Generated by Django 2.1 on 2018-10-23 23:00 + +from django.db import migrations, models +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0099_auto_20180912_1035'), + ] + + operations = [ + migrations.AddField( + model_name='invoice', + name='invoice_from_city', + field=models.CharField(max_length=190, null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_from_country', + field=django_countries.fields.CountryField(max_length=2, null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_from_name', + field=models.CharField(max_length=190, null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_from_tax_id', + field=models.CharField(max_length=190, null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_from_vat_id', + field=models.CharField(max_length=190, null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_from_zipcode', + field=models.CharField(max_length=190, null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_to_city', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_to_company', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_to_country', + field=django_countries.fields.CountryField(max_length=2, null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_to_name', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_to_street', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_to_vat_id', + field=models.TextField(null=True), + ), + migrations.AddField( + model_name='invoice', + name='invoice_to_zipcode', + field=models.CharField(max_length=190, null=True), + ), + ] diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py index 8b9962dd12..097782ff58 100644 --- a/src/pretix/base/models/invoices.py +++ b/src/pretix/base/models/invoices.py @@ -5,6 +5,8 @@ from django.db import DatabaseError, models, transaction from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.functional import cached_property +from django.utils.translation import pgettext +from django_countries.fields import CountryField def invoice_filename(instance, filename: str) -> str: @@ -73,7 +75,20 @@ class Invoice(models.Model): is_cancellation = models.BooleanField(default=False) refers = models.ForeignKey('Invoice', related_name='refered', null=True, blank=True, on_delete=models.CASCADE) invoice_from = models.TextField() + invoice_from_name = models.CharField(max_length=190, null=True) + invoice_from_zipcode = models.CharField(max_length=190, null=True) + invoice_from_city = models.CharField(max_length=190, null=True) + invoice_from_country = CountryField(null=True) + invoice_from_tax_id = models.CharField(max_length=190, null=True) + invoice_from_vat_id = models.CharField(max_length=190, null=True) invoice_to = models.TextField() + invoice_to_company = models.TextField(null=True) + invoice_to_name = models.TextField(null=True) + invoice_to_street = models.TextField(null=True) + invoice_to_zipcode = models.CharField(max_length=190, null=True) + invoice_to_city = models.TextField(null=True) + invoice_to_country = CountryField(null=True) + invoice_to_vat_id = models.TextField(null=True) date = models.DateField(default=today) locale = models.CharField(max_length=50, default='en') introductory_text = models.TextField(blank=True) @@ -92,6 +107,18 @@ class Invoice(models.Model): def _to_numeric_invoice_number(number): return '{:05d}'.format(int(number)) + @property + def full_invoice_from(self): + parts = [ + self.invoice_from_name, + self.invoice_from, + (self.invoice_from_zipcode or "") + " " + (self.invoice_from_city or ""), + str(self.invoice_from_country), + pgettext("invoice", "VAT-ID: %s" % self.invoice_from_vat_id) if self.invoice_from_vat_id else "", + pgettext("invoice", "Tax ID: %s" % self.invoice_from_tax_id) if self.invoice_from_tax_id else "", + ] + return '\n'.join([p.strip() for p in parts if p and p.strip()]) + def _get_numeric_invoice_number(self): numeric_invoices = Invoice.objects.filter( event__organizer=self.event.organizer, diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index 93b2753b7a..734e1fe2cb 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -40,6 +40,12 @@ def build_invoice(invoice: Invoice) -> Invoice: with language(invoice.locale): invoice.invoice_from = invoice.event.settings.get('invoice_address_from') + invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name') + invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode') + invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city') + invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country') + invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id') + invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id') introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString) additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString) @@ -66,8 +72,16 @@ def build_invoice(invoice: Invoice) -> Invoice: country=ia.country.name if ia.country else ia.country_old ).strip() invoice.internal_reference = ia.internal_reference + invoice.invoice_to_company = ia.company + invoice.invoice_to_name = ia.name + invoice.invoice_to_street = ia.street + invoice.invoice_to_zipcode = ia.zipcode + invoice.invoice_to_city = ia.city + invoice.invoice_to_country = ia.country + if ia.vat_id: invoice.invoice_to += "\n" + pgettext("invoice", "VAT-ID: %s") % ia.vat_id + invoice.invoice_to_vat_id = ia.vat_id cc = str(ia.country) @@ -267,6 +281,12 @@ def build_preview_invoice_pdf(event): date=timezone.now().date(), locale=locale, organizer=event.organizer ) invoice.invoice_from = event.settings.get('invoice_address_from') + invoice.invoice_from_name = invoice.event.settings.get('invoice_address_from_name') + invoice.invoice_from_zipcode = invoice.event.settings.get('invoice_address_from_zipcode') + invoice.invoice_from_city = invoice.event.settings.get('invoice_address_from_city') + invoice.invoice_from_country = invoice.event.settings.get('invoice_address_from_country') + invoice.invoice_from_tax_id = invoice.event.settings.get('invoice_address_from_tax_id') + invoice.invoice_from_vat_id = invoice.event.settings.get('invoice_address_from_vat_id') introductory = event.settings.get('invoice_introductory_text', as_type=LazyI18nString) additional = event.settings.get('invoice_additional_text', as_type=LazyI18nString) diff --git a/src/pretix/control/forms/event.py b/src/pretix/control/forms/event.py index 1aff70866e..5b724153c5 100644 --- a/src/pretix/control/forms/event.py +++ b/src/pretix/control/forms/event.py @@ -9,7 +9,7 @@ from django.utils.timezone import get_current_timezone_name from django.utils.translation import ( pgettext, pgettext_lazy, ugettext_lazy as _, ) -from django_countries import Countries +from django_countries import Countries, countries from django_countries.fields import LazyTypedChoiceField from i18nfield.forms import ( I18nForm, I18nFormField, I18nFormSetMixin, I18nTextarea, I18nTextInput, @@ -522,6 +522,9 @@ class ProviderForm(SettingsForm): class InvoiceSettingsForm(SettingsForm): + allcountries = list(countries) + allcountries.insert(0, ('', _('Select country'))) + invoice_address_asked = forms.BooleanField( label=_("Ask for invoice address"), required=False @@ -572,9 +575,10 @@ class InvoiceSettingsForm(SettingsForm): invoice_generate = forms.ChoiceField( label=_("Generate invoices"), required=False, + widget=forms.RadioSelect, choices=( - ('False', _('No')), - ('admin', _('Manually in admin panel')), + ('False', _('Do not generate invoices')), + ('admin', _('Only manually in admin panel')), ('user', _('Automatically on user request')), ('True', _('Automatically for all created orders')), ('paid', _('Automatically on payment')), @@ -598,19 +602,46 @@ class InvoiceSettingsForm(SettingsForm): required=True, choices=[] ) + invoice_address_from_name = forms.CharField( + label=_("Company name"), + required=False, + ) invoice_address_from = forms.CharField( + label=_("Address line"), widget=forms.Textarea(attrs={ - 'rows': 5, + 'rows': 2, 'placeholder': _( - 'Sample Event Company\n' - 'Albert Einstein Road 52\n' - '12345 Samplecity' + 'Albert Einstein Road 52' ) }), required=False, - label=_("Your address"), - help_text=_("Will be printed as the sender on invoices. Be sure to include relevant details required in " - "your jurisdiction.") + ) + invoice_address_from_zipcode = forms.CharField( + widget=forms.TextInput(attrs={ + 'placeholder': '12345' + }), + required=False, + label=_("ZIP code"), + ) + invoice_address_from_city = forms.CharField( + widget=forms.TextInput(attrs={ + 'placeholder': _('Random City') + }), + required=False, + label=_("City"), + ) + invoice_address_from_country = forms.ChoiceField( + choices=allcountries, + required=False, + label=_("Country"), + ) + invoice_address_from_tax_id = forms.CharField( + required=False, + label=_("Domestic tax ID"), + ) + invoice_address_from_vat_id = forms.CharField( + required=False, + label=_("EU VAT ID"), ) invoice_introductory_text = I18nFormField( widget=I18nTextarea, diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index a60008b471..202c6e6763 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -6,21 +6,36 @@ {% csrf_token %} {% bootstrap_form_errors form %}
- {% trans "Invoicing" %} - {% bootstrap_field form.invoice_address_asked layout="control" %} - {% bootstrap_field form.invoice_address_required layout="control" %} - {% bootstrap_field form.invoice_name_required layout="control" %} + {% trans "Invoice settings" %} {% bootstrap_field form.invoice_generate layout="control" %} {% bootstrap_field form.invoice_email_attachment layout="control" %} - {% bootstrap_field form.invoice_address_company_required layout="control" %} - {% bootstrap_field form.invoice_address_vatid layout="control" %} - {% bootstrap_field form.invoice_numbers_consecutive layout="control" %} {% bootstrap_field form.invoice_numbers_prefix layout="control" %} - {% bootstrap_field form.invoice_renderer layout="control" %} + {% bootstrap_field form.invoice_numbers_consecutive layout="control" %} {% bootstrap_field form.invoice_language layout="control" %} {% bootstrap_field form.invoice_include_free layout="control" %} {% bootstrap_field form.invoice_attendee_name layout="control" %} +
+
+ {% trans "Invoice address form" %} + {% bootstrap_field form.invoice_address_asked layout="control" %} + {% bootstrap_field form.invoice_address_required layout="control" %} + {% bootstrap_field form.invoice_name_required layout="control" %} + {% bootstrap_field form.invoice_address_company_required layout="control" %} + {% bootstrap_field form.invoice_address_vatid layout="control" %} +
+
+ {% trans "Your invoice details" %} + {% bootstrap_field form.invoice_address_from_name layout="control" %} {% bootstrap_field form.invoice_address_from layout="control" %} + {% bootstrap_field form.invoice_address_from_zipcode layout="control" %} + {% bootstrap_field form.invoice_address_from_city layout="control" %} + {% bootstrap_field form.invoice_address_from_country layout="control" %} + {% bootstrap_field form.invoice_address_from_tax_id layout="control" %} + {% bootstrap_field form.invoice_address_from_vat_id layout="control" %} +
+
+ {% trans "Invoice customization" %} + {% bootstrap_field form.invoice_renderer layout="control" %} {% bootstrap_field form.invoice_introductory_text layout="control" %} {% bootstrap_field form.invoice_additional_text layout="control" %} {% bootstrap_field form.invoice_footer_text layout="control" %} diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 3ffe0d8281..95de8f60ca 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -59,7 +59,9 @@ class EventsTest(SoupTest): doc.select("[name=ticket_download]")[0]['checked'] = "checked" doc.select("[name=contact_mail]")[0]['value'] = "test@example.org" doc.select("[name=payment_banktransfer__enabled]")[0]['checked'] = "checked" - doc.select("[name*=payment_banktransfer_bank_details]")[0].contents[0].replace_with("Foo") + doc.select("[name=payment_banktransfer_bank_details_type]")[1]['checked'] = 'checked' + del doc.select("[name=payment_banktransfer_bank_details_type]")[0]['checked'] + doc.select("[name*=payment_banktransfer_bank_details_0]")[0].contents[0].replace_with("Foo") doc.select("[name=total_quota]")[0]['value'] = "300" doc.select("[name=form-TOTAL_FORMS]")[0]['value'] = "2" doc.select("[name=form-INITIAL_FORMS]")[0]['value'] = "2" @@ -103,7 +105,9 @@ class EventsTest(SoupTest): doc.select("[name=ticket_download]")[0]['checked'] = "checked" doc.select("[name=contact_mail]")[0]['value'] = "test@example.org" doc.select("[name=payment_banktransfer__enabled]")[0]['checked'] = "checked" - doc.select("[name*=payment_banktransfer_bank_details]")[0].contents[0].replace_with("Foo") + doc.select("[name=payment_banktransfer_bank_details_type]")[1]['checked'] = 'checked' + del doc.select("[name=payment_banktransfer_bank_details_type]")[0]['checked'] + doc.select("[name*=payment_banktransfer_bank_details_0]")[0].contents[0].replace_with("Foo") doc.select("[name=total_quota]")[0]['value'] = "" doc.select("[name=form-TOTAL_FORMS]")[0]['value'] = "2" doc.select("[name=form-INITIAL_FORMS]")[0]['value'] = "2" @@ -151,7 +155,9 @@ class EventsTest(SoupTest): doc.select("[name=ticket_download]")[0]['checked'] = "checked" doc.select("[name=contact_mail]")[0]['value'] = "test@example.org" doc.select("[name=payment_banktransfer__enabled]")[0]['checked'] = "checked" - doc.select("[name*=payment_banktransfer_bank_details]")[0].contents[0].replace_with("Foo") + doc.select("[name=payment_banktransfer_bank_details_type]")[1]['checked'] = 'checked' + del doc.select("[name=payment_banktransfer_bank_details_type]")[0]['checked'] + doc.select("[name*=payment_banktransfer_bank_details_0]")[0].contents[0].replace_with("Foo") doc.select("[name=total_quota]")[0]['value'] = "120" doc.select("[name=form-TOTAL_FORMS]")[0]['value'] = "2" doc.select("[name=form-INITIAL_FORMS]")[0]['value'] = "2" @@ -285,6 +291,7 @@ class EventsTest(SoupTest): self.post_doc('/control/event/%s/%s/settings/payment/banktransfer' % (self.orga1.slug, self.event1.slug), { 'payment_banktransfer__enabled': 'true', 'payment_banktransfer__fee_abs': '12.23', + 'payment_banktransfer_bank_details_type': 'other', 'payment_banktransfer_bank_details_0': 'Test', }) self.event1.settings.flush()