Compare commits

...

19 Commits

Author SHA1 Message Date
Richard Schreiber
32ee37fed6 add missing fineprint-size for single-price-line without taxes 2025-11-06 08:35:21 +01:00
Richard Schreiber
8e78ad29ed reduce padding between first and following lines 2025-11-06 08:33:11 +01:00
Richard Schreiber
29d9080545 make period_line and single_price_line fineprint-size 2025-11-06 08:32:54 +01:00
Richard Schreiber
d25eb0fd81 don’t potentially create an infinite loop 2025-11-06 08:26:04 +01:00
Richard Schreiber
af5711efdd Further finetune comment regarding trimming 2025-11-06 08:26:04 +01:00
Richard Schreiber
5f44f430ec improve comment regarding trimming 2025-11-06 08:26:04 +01:00
Richard Schreiber
a34481d90c fix flake8 issues 2025-11-06 08:26:04 +01:00
Richard Schreiber
cd8d3c1f37 base max_height on lineheight with 5 respectively 8 lines 2025-11-06 08:26:04 +01:00
Richard Schreiber
7d7f27eb3d fix newline issues 2025-11-06 08:26:04 +01:00
Richard Schreiber
7d34cf9f3f improve chunking preformance 2025-11-06 08:26:04 +01:00
Richard Schreiber
97b6a7f97a fix textwrap removing whitespace/newlines 2025-11-06 08:26:04 +01:00
Richard Schreiber
5dc8610be5 fix max_width missing cellpadding 2025-11-06 08:26:04 +01:00
Richard Schreiber
3fe42dc54f fix flake8 2025-11-06 08:26:04 +01:00
Richard Schreiber
87ba02ffa6 span description over 2 or 3 columns for more space 2025-11-06 08:26:04 +01:00
Richard Schreiber
11beca6a92 re-add inner-padding between first line and description 2025-11-06 08:26:04 +01:00
Richard Schreiber
bf415a6b02 remove inner-padding from description-rows 2025-11-06 08:26:04 +01:00
Richard Schreiber
ad51edf488 refactor description-splitting 2025-11-06 08:26:04 +01:00
Richard Schreiber
166da7a7d2 split description into paragraphs to enable pagebreak 2025-11-06 08:26:03 +01:00
Richard Schreiber
eb872e7d35 Trim line description on invoice PDF to always fit on one page 2025-11-06 08:26:03 +01:00

View File

@@ -23,6 +23,7 @@ import datetime
import logging import logging
import math import math
import re import re
import textwrap
import unicodedata import unicodedata
from collections import defaultdict from collections import defaultdict
from decimal import Decimal from decimal import Decimal
@@ -752,11 +753,59 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
return dt.astimezone(tz).date() return dt.astimezone(tz).date()
total = Decimal('0.00') total = Decimal('0.00')
if has_taxes:
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else:
colwidths = [a * doc.width for a in (.65, .20, .15)]
for (description, tax_rate, tax_name, net_value, gross_value, subevent, period_start, period_end), lines in addon_aware_groupby( for (description, tax_rate, tax_name, net_value, gross_value, subevent, period_start, period_end), lines in addon_aware_groupby(
all_lines, all_lines,
key=_group_key, key=_group_key,
is_addon=lambda l: l.description.startswith(" +"), is_addon=lambda l: l.description.startswith(" +"),
): ):
# split description into multiple Paragraphs so each fits in a table cell on a single page
# otherwise PDF-build fails
description_p_list = []
# normalize linebreaks to newlines instead of HTML so we can safely substring
description = description.replace('<br>', '<br />').replace('<br />\n', '\n').replace('<br />', '\n')
# start first line with different settings than the rest of the description
curr_description = description.split("\n", maxsplit=1)[0]
cellpadding = 6 # default cellpadding is only set on right side of column
max_width = colwidths[0] - cellpadding
max_height = self.stylesheet['Normal'].leading * 5
p_style = self.stylesheet['Normal']
for __ in range(1000):
p = FontFallbackParagraph(
self._clean_text(curr_description, tags=['br']),
p_style
)
h = p.wrap(max_width, doc.height)[1]
if h <= max_height:
description_p_list.append(p)
if curr_description == description:
break
description = description[len(curr_description):].lstrip()
curr_description = description.split("\n", maxsplit=1)[0]
# use different settings for all except first line
max_width = sum(colwidths[0:3 if has_taxes else 2]) - cellpadding
max_height = self.stylesheet['Fineprint'].leading * 8
p_style = self.stylesheet['Fineprint']
continue
if not description_p_list:
# first "manual" line is larger than 5 "real" lines => only allow one line and set rest in Fineprint
max_height = self.stylesheet['Normal'].leading
if h > max_height * 1.1:
# quickly bring the text-length down to a managable length to then stepwise reduce
wrap_to = math.ceil(len(curr_description) * max_height * 1.1 / h)
else:
# trim to 95% length, but at most 10 chars to not have strangely short lines in the middle of a paragraph
wrap_to = max(len(curr_description) - 10, math.ceil(len(curr_description) * 0.95))
curr_description = textwrap.wrap(curr_description, wrap_to, replace_whitespace=False, drop_whitespace=False)[0]
# Try to be clever and figure out when organizers would want to show the period. This heuristic is # Try to be clever and figure out when organizers would want to show the period. This heuristic is
# not perfect and the only "fully correct" way would be to include the period on every line always, # not perfect and the only "fully correct" way would be to include the period on every line always,
# however this will cause confusion (a) due to useless repetition of the same date all over the invoice # however this will cause confusion (a) due to useless repetition of the same date all over the invoice
@@ -810,7 +859,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
# Group together at the end of the invoice # Group together at the end of the invoice
request_show_service_date = period_line request_show_service_date = period_line
elif period_line: elif period_line:
description += "\n" + period_line description_p_list.append(FontFallbackParagraph(
period_line,
self.stylesheet['Fineprint']
))
lines = list(lines) lines = list(lines)
if has_taxes: if has_taxes:
@@ -819,13 +871,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
net_price=money_filter(net_value, self.invoice.event.currency), net_price=money_filter(net_value, self.invoice.event.currency),
gross_price=money_filter(gross_value, self.invoice.event.currency), gross_price=money_filter(gross_value, self.invoice.event.currency),
) )
description = description + "\n" + single_price_line description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append(( tdata.append((
FontFallbackParagraph( description_p_list.pop(0),
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
str(len(lines)), str(len(lines)),
localize(tax_rate) + " %", localize(tax_rate) + " %",
FontFallbackParagraph( FontFallbackParagraph(
@@ -837,23 +889,52 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
)) ))
for p in description_p_list:
tdata.append((p, "", "", "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(2, len(tdata) - 1),
))
else: else:
if len(lines) > 1: if len(lines) > 1:
single_price_line = pgettext('invoice', 'Single price: {price}').format( single_price_line = pgettext('invoice', 'Single price: {price}').format(
price=money_filter(gross_value, self.invoice.event.currency), price=money_filter(gross_value, self.invoice.event.currency),
) )
description = description + "\n" + single_price_line description_p_list.append(FontFallbackParagraph(
single_price_line,
self.stylesheet['Fineprint']
))
tdata.append(( tdata.append((
FontFallbackParagraph( description_p_list.pop(0),
self._clean_text(description, tags=['br']),
self.stylesheet['Normal']
),
str(len(lines)), str(len(lines)),
FontFallbackParagraph( FontFallbackParagraph(
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '), money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
self.stylesheet['NormalRight'] self.stylesheet['NormalRight']
), ),
)) ))
for p in description_p_list:
tdata.append((p, "", ""))
tstyledata.append((
'SPAN',
(0, len(tdata) - 1),
(1, len(tdata) - 1),
))
tstyledata += [
(
'BOTTOMPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 2),
0
),
(
'TOPPADDING',
(0, len(tdata) - len(description_p_list)),
(-1, len(tdata) - 1),
0
),
]
taxvalue_map[tax_rate, tax_name] += (gross_value - net_value) * len(lines) taxvalue_map[tax_rate, tax_name] += (gross_value - net_value) * len(lines)
grossvalue_map[tax_rate, tax_name] += gross_value * len(lines) grossvalue_map[tax_rate, tax_name] += gross_value * len(lines)
total += gross_value * len(lines) total += gross_value * len(lines)
@@ -863,13 +944,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '', FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
colwidths = [a * doc.width for a in (.50, .05, .15, .15, .15)]
else: else:
tdata.append([ tdata.append([
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
money_filter(total, self.invoice.event.currency) money_filter(total, self.invoice.event.currency)
]) ])
colwidths = [a * doc.width for a in (.65, .20, .15)]
if not self.invoice.is_cancellation: if not self.invoice.is_cancellation:
if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING: if self.invoice.event.settings.invoice_show_payments and self.invoice.order.status == Order.STATUS_PENDING: