diff --git a/doc/api/resources/invoices.rst b/doc/api/resources/invoices.rst index e8ec492408..85d11943c6 100644 --- a/doc/api/resources/invoices.rst +++ b/doc/api/resources/invoices.rst @@ -42,6 +42,7 @@ introductory_text string Text to be prin additional_text string Text to be printed below the product list payment_provider_text string Text to be printed below the product list with payment information +payment_provider_stamp string Short text to be visibly printed to indicate payment status footer_text string Text to be printed in the page footer area lines list of objects The actual invoice contents ├ position integer Number of the line within an invoice. @@ -178,6 +179,7 @@ Endpoints "internal_reference": "", "additional_text": "We are looking forward to see you on our conference!", "payment_provider_text": "Please transfer the money to our account ABC…", + "payment_provider_stamp": null, "footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321", "lines": [ { @@ -268,6 +270,7 @@ Endpoints "internal_reference": "", "additional_text": "We are looking forward to see you on our conference!", "payment_provider_text": "Please transfer the money to our account ABC…", + "payment_provider_stamp": null, "footer_text": "Big Events LLC - Registration No. 123456 - VAT ID: EU0987654321", "lines": [ { diff --git a/doc/development/api/payment.rst b/doc/development/api/payment.rst index a29b0ce6bd..65da00d3bd 100644 --- a/doc/development/api/payment.rst +++ b/doc/development/api/payment.rst @@ -102,6 +102,8 @@ The provider class .. automethod:: render_invoice_text + .. automethod:: render_invoice_stamp + .. automethod:: order_change_allowed .. automethod:: payment_prepare diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index b44a74b9c4..794e745d2a 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -1484,9 +1484,9 @@ class InvoiceSerializer(I18nAwareModelSerializer): 'invoice_to', 'invoice_to_company', 'invoice_to_name', 'invoice_to_street', 'invoice_to_zipcode', 'invoice_to_city', 'invoice_to_state', 'invoice_to_country', 'invoice_to_vat_id', 'invoice_to_beneficiary', 'custom_field', 'date', 'refers', 'locale', - 'introductory_text', 'additional_text', 'payment_provider_text', 'footer_text', 'lines', - 'foreign_currency_display', 'foreign_currency_rate', 'foreign_currency_rate_date', - 'internal_reference') + 'introductory_text', 'additional_text', 'payment_provider_text', 'payment_provider_stamp', + 'footer_text', 'lines', 'foreign_currency_display', 'foreign_currency_rate', + 'foreign_currency_rate_date', 'internal_reference') class OrderPaymentCreateSerializer(I18nAwareModelSerializer): diff --git a/src/pretix/base/invoice.py b/src/pretix/base/invoice.py index b52979fe64..0fffe9d686 100644 --- a/src/pretix/base/invoice.py +++ b/src/pretix/base/invoice.py @@ -35,8 +35,8 @@ from django.utils.formats import date_format, localize from django.utils.translation import ( get_language, gettext, gettext_lazy, pgettext, ) -from reportlab.lib import pagesizes -from reportlab.lib.enums import TA_LEFT, TA_RIGHT +from reportlab.lib import colors, pagesizes +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT from reportlab.lib.styles import ParagraphStyle, StyleSheet1 from reportlab.lib.units import mm from reportlab.pdfbase import pdfmetrics @@ -44,8 +44,8 @@ from reportlab.pdfbase.pdfmetrics import stringWidth from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen.canvas import Canvas from reportlab.platypus import ( - BaseDocTemplate, Frame, KeepTogether, NextPageTemplate, PageTemplate, - Paragraph, Spacer, Table, TableStyle, + BaseDocTemplate, Flowable, Frame, KeepTogether, NextPageTemplate, + PageTemplate, Paragraph, Spacer, Table, TableStyle, ) from pretix.base.decimal import round_decimal @@ -147,6 +147,8 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): """ stylesheet = StyleSheet1() stylesheet.add(ParagraphStyle(name='Normal', fontName=self.font_regular, fontSize=10, leading=12)) + stylesheet.add(ParagraphStyle(name='BoldInverseCenter', fontName=self.font_bold, fontSize=10, leading=12, + textColor=colors.white, alignment=TA_CENTER)) stylesheet.add(ParagraphStyle(name='InvoiceFrom', parent=stylesheet['Normal'])) stylesheet.add(ParagraphStyle(name='Heading1', fontName=self.font_bold, fontSize=15, leading=15 * 1.2)) stylesheet.add(ParagraphStyle(name='FineprintHeading', fontName=self.font_bold, fontSize=8, leading=12)) @@ -249,6 +251,31 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer): ).strip().replace('
', '
').replace('\n', '
\n') +class PaidMarker(Flowable): + def __init__(self, text='paid', color=None, font='OpenSansBd', size=20): + super().__init__() + self.text = text + self.color = color + self.font = font + self.size = size + self._showBoundary = True + + def wrap(self, availwidth, availheight): + # Fake a size, we don't care if we exceed the table + return 10, self.size / 2 + + def draw(self): + self.canv.translate(0, - self.size / 2) + self.canv.rotate(2) + self.canv.setFont(self.font, self.size) + self.canv.setFillColor(self.color) + width = self.canv.stringWidth(self.text, self.font, self.size) + self.canv.drawRightString(0, 0, self.text) + + self.canv.setStrokeColor(self.color) + self.canv.roundRect(-width - self.size / 2, -self.size / 4, width + self.size, self.size + self.size / 4, 3) + + class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): identifier = 'classic' verbose_name = pgettext('invoice', 'Classic renderer (pretix 1.0)') @@ -612,10 +639,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): tdata.append([ pgettext('invoice', 'Invoice total'), '', money_filter(total, self.invoice.event.currency) ]) - colwidths = [a * doc.width for a in (.65, .05, .30)] + colwidths = [a * doc.width for a in (.65, .20, .15)] - if self.invoice.event.settings.invoice_show_payments and not self.invoice.is_cancellation: - if self.invoice.order.status == Order.STATUS_PENDING: + if not self.invoice.is_cancellation: + if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING: pending_sum = self.invoice.order.pending_sum if pending_sum != total: tdata.append([pgettext('invoice', 'Received payments')] + (['', '', ''] if has_taxes else ['']) + [ @@ -627,7 +654,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): tstyledata += [ ('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold), ] - elif self.invoice.order.payments.filter( + elif self.invoice.event.settings.invoice_show_payments and self.invoice.order.payments.filter( state__in=(OrderPayment.PAYMENT_STATE_CONFIRMED, OrderPayment.PAYMENT_STATE_REFUNDED), provider='giftcard' ).exists(): giftcard_sum = self.invoice.order.payments.filter( @@ -645,6 +672,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): tstyledata += [ ('FONTNAME', (0, len(tdata) - 3), (-1, len(tdata) - 3), self.font_bold), ] + elif self.invoice.payment_provider_stamp: + pm = PaidMarker( + text=self.invoice.payment_provider_stamp, + color=colors.HexColor(self.event.settings.theme_color_success), + size=16 + ) + tdata[-1][-2] = pm table = Table(tdata, colWidths=colwidths, repeatRows=1) table.setStyle(TableStyle(tstyledata)) diff --git a/src/pretix/base/migrations/0229_invoice_payment_provider_stamp.py b/src/pretix/base/migrations/0229_invoice_payment_provider_stamp.py new file mode 100644 index 0000000000..da75d2d98e --- /dev/null +++ b/src/pretix/base/migrations/0229_invoice_payment_provider_stamp.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.17 on 2023-02-07 10:00 + +import django.core.serializers.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0228_scheduledeventexport_scheduledorganizerexport'), + ] + + operations = [ + migrations.AddField( + model_name='invoice', + name='payment_provider_stamp', + field=models.CharField(max_length=100, null=True), + ), + ] diff --git a/src/pretix/base/models/invoices.py b/src/pretix/base/models/invoices.py index ece97d6f96..624fd5d934 100644 --- a/src/pretix/base/models/invoices.py +++ b/src/pretix/base/models/invoices.py @@ -95,6 +95,8 @@ class Invoice(models.Model): :type additional_text: str :param payment_provider_text: A payment provider specific text :type payment_provider_text: str + :param payment_provider_stamp: A payment provider specific stamp + :type payment_provider_stamp: str :param footer_text: A footer text, displayed smaller and centered on every page :type footer_text: str :param foreign_currency_display: A different currency that taxes should also be displayed in. @@ -144,6 +146,7 @@ class Invoice(models.Model): additional_text = models.TextField(blank=True) reverse_charge = models.BooleanField(default=False) payment_provider_text = models.TextField(blank=True) + payment_provider_stamp = models.CharField(max_length=100, null=True, blank=True) footer_text = models.TextField(blank=True) foreign_currency_display = models.CharField(max_length=50, null=True, blank=True) diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 4901a7b07d..95b5ce9dd0 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -464,6 +464,16 @@ class BasePaymentProvider: return pgettext_lazy('invoice', 'The payment for this invoice has already been received.') return self.settings.get('_invoice_text', as_type=LazyI18nString, default='') + def render_invoice_stamp(self, order: Order, payment: OrderPayment) -> str: + """ + This is called when an invoice for an order with this payment provider is generated. + The default implementation returns "paid" if the order was already paid, and ``None`` + otherwise. You can override this with a string, but it should be *really* short to make + the invoice look pretty. + """ + if order.status == Order.STATUS_PAID: + return _('paid') + @property def payment_form_fields(self) -> dict: """ diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index d5f68e4572..c5aa16dd0b 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -98,8 +98,10 @@ def build_invoice(invoice: Invoice) -> Invoice: payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp)) else: payment = str(lp.payment_provider.render_invoice_text(invoice.order)) + payment_stamp = lp.payment_provider.render_invoice_stamp(invoice.order, lp) else: payment = "" + payment_stamp = None if invoice.event.settings.invoice_include_expire_date and invoice.order.status == Order.STATUS_PENDING: if payment: payment += "

" @@ -111,6 +113,7 @@ def build_invoice(invoice: Invoice) -> Invoice: invoice.additional_text = str(additional).replace('\n', '
') invoice.footer_text = str(footer) invoice.payment_provider_text = str(payment).replace('\n', '
') + invoice.payment_provider_stamp = str(payment_stamp) if payment_stamp else None try: ia = invoice.order.invoice_address @@ -325,6 +328,7 @@ def generate_cancellation(invoice: Invoice, trigger_pdf=True): cancellation.is_cancellation = True cancellation.date = timezone.now().date() cancellation.payment_provider_text = '' + cancellation.payment_provider_stamp = '' cancellation.file = None cancellation.sent_to_organizer = None cancellation.sent_to_customer = None @@ -436,6 +440,7 @@ def build_preview_invoice_pdf(event): invoice.additional_text = str(additional).replace('\n', '
') invoice.footer_text = str(footer) invoice.payment_provider_text = str(payment).replace('\n', '
') + invoice.payment_provider_stamp = _('paid') invoice.invoice_to_name = _("John Doe") invoice.invoice_to_street = _("214th Example Street") invoice.invoice_to_zipcode = _("012345") diff --git a/src/tests/api/test_invoices.py b/src/tests/api/test_invoices.py index cefe963161..a158e44a1e 100644 --- a/src/tests/api/test_invoices.py +++ b/src/tests/api/test_invoices.py @@ -176,6 +176,7 @@ TEST_INVOICE_RES = { "internal_reference": "", "additional_text": "", "payment_provider_text": "", + "payment_provider_stamp": None, "footer_text": "", "foreign_currency_display": None, "foreign_currency_rate": None, diff --git a/src/tests/api/test_order_change.py b/src/tests/api/test_order_change.py index da78eba7a7..6aaba9781e 100644 --- a/src/tests/api/test_order_change.py +++ b/src/tests/api/test_order_change.py @@ -445,6 +445,7 @@ def test_order_create_invoice(token_client, organizer, event, order): 'introductory_text': '', 'additional_text': '', 'payment_provider_text': '', + 'payment_provider_stamp': None, 'footer_text': '', 'lines': [ {