From b554fc1ad5c360a0d85edc71f7fbf18157712a8d Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 13 Jan 2025 22:04:03 +0100 Subject: [PATCH] Invoice renderer: Group invoice lines even with addons (Z#23173618) --- src/pretix/base/invoice.py | 63 +++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/pretix/base/invoice.py b/src/pretix/base/invoice.py index 30de1433e..55307a8b9 100644 --- a/src/pretix/base/invoice.py +++ b/src/pretix/base/invoice.py @@ -62,6 +62,63 @@ from pretix.presale.style import get_fonts logger = logging.getLogger(__name__) +def addon_aware_groupby(iterable, key, is_addon): + """ + We use groupby() to visually group identical lines on an invoice. For example, instead of + + Product 1 5.00 EUR + Product 1 5.00 EUR + Product 1 5.00 EUR + Product 2 7.00 EUR + + We want to print + + 3x Product 1 5.00 EUR = 15.00 EUR + Product 2 7.00 EUR + + However, this fails for setups with addon-products since groupby() only groups consecutive + lines with the same identity. So in + + Product 1 5.00 EUR + + Addon 1 2.00 EUR + Product 1 5.00 EUR + + Addon 1 2.00 EUR + Product 1 5.00 EUR + + Addon 2 3.00 EUR + + There is no consecutive repetition of the same entity. This function provides a specialised groupby which + understands the product/addon relationship and packs groups of these addons together if they are, in fact, + identical groups: + + 2x Product 1 5.00 EUR = 10.00 EUR + + 2x Addon 1 2.00 EUR = 4.00 EUR + Product 1 5.00 EUR + + Addon 2 3.00 EUR + """ + packed_groups = [] + + for i in iterable: + if is_addon(i): + packed_groups[-1][1].append(i) + else: + packed_groups.append((i, [])) + # Each packed_groups element contains (parent product, list of addon products) + + def _reorder(packed_groups): + # Emit the products as individual products again, reordered by "all parent products, then all addon products" + # within each group. + for group_grouper, grouped in groupby(packed_groups, key=lambda g: key(g[0]) + tuple(key(a) for a in g[1])): + grouped = list(grouped) + for grp in grouped: + yield grp[0] + for i in range(max(len(grp[1]) for grp in grouped)): + for grp in grouped: + if len(grp[1]) > i: + yield grp[1][i] + + return groupby(_reorder(packed_groups), key) + + class NumberedCanvas(Canvas): def __init__(self, *args, **kwargs): self.font_regular = kwargs.pop('font_regular') @@ -635,7 +692,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer): line.event_date_from, line.event_date_to) total = Decimal('0.00') - for (description, tax_rate, tax_name, net_value, gross_value, *ignored), lines in groupby(self.invoice.lines.all(), key=_group_key): + for (description, tax_rate, tax_name, net_value, gross_value, *ignored), lines in addon_aware_groupby( + self.invoice.lines.all(), + key=_group_key, + is_addon=lambda l: l.description.startswith(" +"), + ): lines = list(lines) if has_taxes: if len(lines) > 1: