Merge pull request #624 from DRYTRIX/rc/v5.5.7

Rc/v5.5.7
This commit is contained in:
Dries Peeters
2026-05-14 21:11:34 +02:00
committed by GitHub
5 changed files with 206 additions and 62 deletions
+16
View File
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [5.5.7] - 2026-05-14
### Fixed
- **Invoice PDF designer layout** — Restored the missing canvas-area wrapper in the invoice PDF designer so the properties panel sits in the third grid column beside the canvas instead of stacking below it (`app/templates/admin/pdf_layout.html`).
- **Invoice PDF preview vs export (#622)** — The JSON-to-HTML preview path now uses the same table style keys as export (header and row text, row background, border width) so the preview matches generated PDFs.
### Changed
- **Designer template JSON and ReportLab export** — Saving template JSON from the designer reads items-table and expenses-table width, colors, and separator line settings from the Konva group children; column widths scale to the chosen table width and a style block is emitted for ReportLab (`app/routes/admin.py`, `pdf_layout.html`).
- **ReportLab invoice tables** — Column widths scale to `element.width`; tables are wrapped in a two-column outer table so horizontal offset from the left margin is honored; `borderColor` and `borderWidth` from template style are applied (`app/utils/pdf_generator_reportlab.py`).
### Documentation
- **Version** — Documented release **5.5.7** to match `setup.py` (single source of truth for the application version).
## [5.5.6] - 2026-05-14
### Documentation
+12 -3
View File
@@ -498,10 +498,19 @@ body {{
table_style = element.get("style", {})
border_color = format_color(table_style.get("borderColor", "#000000"))
header_bg = format_color(table_style.get("headerBackground", "#f8f9fa"))
header_text_color = format_color(table_style.get("headerTextColor", "#000000"))
row_bg = format_color(table_style.get("rowBackground", "#ffffff"))
row_text_color = format_color(table_style.get("rowTextColor", "#000000"))
try:
border_w = float(table_style.get("borderWidth", 1))
except (TypeError, ValueError):
border_w = 1.0
if border_w < 0.5:
border_w = 0.5
style_parts = [style_str_base]
style_parts.append(f"width: {width_px_elem}px")
style_parts.append(f"border: 1px solid {border_color}")
style_parts.append(f"border: {border_w}px solid {border_color}")
style_str = "; ".join(style_parts) + ";"
table_html = f'<table class="element table-element" style="{style_str}"><thead><tr>'
@@ -512,7 +521,7 @@ body {{
align = col.get("align", "left")
col_width = col.get("width", None)
width_attr = f' width="{int(col_width * 96 / 72)}px"' if col_width else ""
table_html += f'<th style="text-align: {align}; background-color: {header_bg};"{width_attr}>{html_escape.escape(header)}</th>'
table_html += f'<th style="text-align: {align}; background-color: {header_bg}; color: {header_text_color};"{width_attr}>{html_escape.escape(header)}</th>'
table_html += "</tr></thead><tbody>"
# Resolve table data from element's data source (e.g. invoice.all_line_items or invoice.items)
@@ -576,7 +585,7 @@ body {{
value = ""
value_escaped = html_escape.escape(str(value))
table_html += f'<td style="text-align: {align};">{value_escaped}</td>'
table_html += f'<td style="text-align: {align}; color: {row_text_color}; background-color: {row_bg};">{value_escaped}</td>'
table_html += "</tr>"
else:
# No data available, show template placeholders
+96 -40
View File
@@ -2263,6 +2263,7 @@
</div>
<div id="pdf-marquee" class="pdf-editor-marquee"></div>
</div>
</div>
<!-- RIGHT: Properties Panel -->
<div class="properties-panel">
@@ -6120,7 +6121,14 @@ async function initializePDFEditor() {
// Extract actual header text from the table group's first Text child
const children = child.getChildren ? child.getChildren() : (child.children || []);
const textElements = children.filter(c => c.className === 'Text');
const headerText = textElements[0] ? (textElements[0].attrs.text || '') : '';
const lineElements = children.filter(c => c.className === 'Line');
const headerEl = textElements[0];
const itemsEl = textElements[1];
const lineEl = lineElements[0];
const headerAttrs = headerEl ? headerEl.attrs : {};
const itemsAttrs = itemsEl ? itemsEl.attrs : {};
const lineAttrs = lineEl ? lineEl.attrs : {};
const headerText = headerEl ? (headerAttrs.text || '') : '';
// Parse header text (format: "Description | Qty | Price | Total" or German equivalent)
// Default to English if header text is empty or doesn't contain |
@@ -6132,6 +6140,19 @@ async function initializePDFEditor() {
headerParts.push(['Description', 'Qty', 'Unit Price', 'Total'][headerParts.length]);
}
}
const tableWidthPx = Math.max(50, Math.round(headerAttrs.width || itemsAttrs.width || 515));
const itemsFracs = [250 / 540, 70 / 540, 110 / 540, 110 / 540];
const totalPtItems = pxToPt(tableWidthPx);
let colPtsItems = itemsFracs.map(f => f * totalPtItems);
const sumCI = colPtsItems.reduce((a, b) => a + b, 0);
if (sumCI > 0) {
colPtsItems = colPtsItems.map(p => (p * totalPtItems) / sumCI);
}
const headerTextHex = rgbToHex(headerAttrs.fill || '#000000');
const itemsTextHex = rgbToHex(itemsAttrs.fill || '#000000');
const lineStrokeHex = rgbToHex(lineAttrs.stroke || '#333333');
const _rawLineW = Number(lineAttrs.strokeWidth);
const lineStrokeW = (Number.isFinite(_rawLineW) && _rawLineW > 0) ? _rawLineW : 1;
// Add to ReportLab template JSON - items table
{% raw %}
@@ -6147,39 +6168,47 @@ async function initializePDFEditor() {
type: 'table',
x: pxToPt(x),
y: pxToPt(y),
width: pxToPt(515),
width: totalPtItems,
opacity: opacity,
columns: [
{width: 250, header: headerParts[0] || 'Description', field: 'description', align: 'left'},
{width: 70, header: headerParts[1] || 'Qty', field: 'quantity', align: 'center'},
{width: 110, header: headerParts[2] || 'Unit Price', field: 'unit_price', align: 'right'},
{width: 110, header: headerParts[3] || 'Total', field: 'total_amount', align: 'right'}
{width: colPtsItems[0], header: headerParts[0] || 'Description', field: 'description', align: 'left'},
{width: colPtsItems[1], header: headerParts[1] || 'Qty', field: 'quantity', align: 'center'},
{width: colPtsItems[2], header: headerParts[2] || 'Unit Price', field: 'unit_price', align: 'right'},
{width: colPtsItems[3], header: headerParts[3] || 'Total', field: 'total_amount', align: 'right'}
],
data: itemsData,
row_template: itemsRowTemplate
row_template: itemsRowTemplate,
style: {
headerBackground: '#f8f9fa',
headerTextColor: headerTextHex,
rowBackground: '#ffffff',
rowTextColor: itemsTextHex,
borderColor: lineStrokeHex,
borderWidth: lineStrokeW
}
});
// Legacy HTML for preview
bodyContent += ` <!-- Items Table Start -->\n`;
bodyContent += ` <div style="position:absolute;left:${x}px;top:${y}px;opacity:${opacity};width:515px;">\n`;
bodyContent += ` <div style="position:absolute;left:${x}px;top:${y}px;opacity:${opacity};width:${tableWidthPx}px;">\n`;
bodyContent += ` <table style="width:100%;border-collapse:collapse;font-size:11px;background:white;">\n`;
bodyContent += ` <thead>\n`;
bodyContent += ` <tr style="background-color:#f8f9fa;border-bottom:2px solid #333;">\n`;
bodyContent += ` <th style="text-align:left;padding:10px;font-weight:bold;font-size:12px;">${(headerParts[0] || 'Description').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:center;padding:10px;font-weight:bold;font-size:12px;width:70px;">${(headerParts[1] || 'Qty').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:right;padding:10px;font-weight:bold;font-size:12px;width:110px;">${(headerParts[2] || 'Unit Price').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:right;padding:10px;font-weight:bold;font-size:12px;width:110px;">${(headerParts[3] || 'Total').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <tr style="background-color:#f8f9fa;border-bottom:${lineStrokeW}px solid ${lineStrokeHex};">\n`;
bodyContent += ` <th style="text-align:left;padding:10px;font-weight:bold;font-size:12px;color:${headerTextHex};">${(headerParts[0] || 'Description').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:center;padding:10px;font-weight:bold;font-size:12px;width:70px;color:${headerTextHex};">${(headerParts[1] || 'Qty').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:right;padding:10px;font-weight:bold;font-size:12px;width:110px;color:${headerTextHex};">${(headerParts[2] || 'Unit Price').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:right;padding:10px;font-weight:bold;font-size:12px;width:110px;color:${headerTextHex};">${(headerParts[3] || 'Total').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` </tr>\n`;
bodyContent += ` </thead>\n`;
bodyContent += ` <tbody>\n`;
{% raw %}
bodyContent += ` {% if invoice.all_line_items %}\n`;
bodyContent += ` {% for item in invoice.all_line_items %}\n`;
bodyContent += ` <tr style="border-bottom:1px solid #ddd;">\n`;
bodyContent += ` <td style="padding:10px;vertical-align:top;">{{ item.description }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:center;vertical-align:top;">{{ item.quantity }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:right;vertical-align:top;">{{ format_money(item.unit_price) }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:right;vertical-align:top;font-weight:bold;">{{ format_money(item.total_amount) }}</td>\n`;
bodyContent += ` <tr style="border-bottom:${lineStrokeW}px solid ${lineStrokeHex};">\n`;
bodyContent += ` <td style="padding:10px;vertical-align:top;color:${itemsTextHex};">{{ item.description }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:center;vertical-align:top;color:${itemsTextHex};">{{ item.quantity }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:right;vertical-align:top;color:${itemsTextHex};">{{ format_money(item.unit_price) }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:right;vertical-align:top;font-weight:bold;color:${itemsTextHex};">{{ format_money(item.total_amount) }}</td>\n`;
bodyContent += ` </tr>\n`;
bodyContent += ` {% endfor %}\n`;
bodyContent += ` {% else %}\n`;
@@ -6196,7 +6225,14 @@ async function initializePDFEditor() {
// Extract actual header text from the table group's first Text child
const children = child.getChildren ? child.getChildren() : (child.children || []);
const textElements = children.filter(c => c.className === 'Text');
const headerText = textElements[0] ? (textElements[0].attrs.text || '') : '';
const lineElements = children.filter(c => c.className === 'Line');
const headerEl = textElements[0];
const itemsEl = textElements[1];
const lineEl = lineElements[0];
const headerAttrs = headerEl ? headerEl.attrs : {};
const itemsAttrs = itemsEl ? itemsEl.attrs : {};
const lineAttrs = lineEl ? lineEl.attrs : {};
const headerText = headerEl ? (headerAttrs.text || '') : '';
// Parse header text (format: "Expense | Date | Category | Amount" or localized)
// Default to English if header text is empty or doesn't contain |
@@ -6208,6 +6244,24 @@ async function initializePDFEditor() {
headerParts.push(['Expense', 'Date', 'Category', 'Amount'][headerParts.length]);
}
}
const defExpHeaderBg = '#fff3cd';
const defExpHeaderText = '#856404';
const defExpRowBg = '#fffbf0';
const defExpRowText = '#856404';
const defExpBorder = '#856404';
const tableWidthPxExp = Math.max(50, Math.round(headerAttrs.width || itemsAttrs.width || 515));
const expFracs = [200 / 515, 100 / 515, 105 / 515, 110 / 515];
const totalPtExp = pxToPt(tableWidthPxExp);
let colPtsExp = expFracs.map(f => f * totalPtExp);
const sumCE = colPtsExp.reduce((a, b) => a + b, 0);
if (sumCE > 0) {
colPtsExp = colPtsExp.map(p => (p * totalPtExp) / sumCE);
}
const expHeaderTextHex = rgbToHex(headerAttrs.fill || defExpHeaderText);
const expItemsTextHex = rgbToHex(itemsAttrs.fill || defExpRowText);
const expLineStrokeHex = rgbToHex(lineAttrs.stroke || defExpBorder);
const _rawExpLW = Number(lineAttrs.strokeWidth);
const expLineStrokeW = (Number.isFinite(_rawExpLW) && _rawExpLW > 0) ? _rawExpLW : 1;
// Add to ReportLab template JSON - expenses table
{% raw %}
@@ -6223,45 +6277,47 @@ async function initializePDFEditor() {
type: 'table',
x: pxToPt(x),
y: pxToPt(y),
width: pxToPt(515),
width: totalPtExp,
opacity: opacity,
columns: [
{width: 200, header: headerParts[0] || 'Expense', field: 'title', align: 'left'},
{width: 100, header: headerParts[1] || 'Date', field: 'expense_date', align: 'center'},
{width: 105, header: headerParts[2] || 'Category', field: 'category', align: 'left'},
{width: 110, header: headerParts[3] || 'Amount', field: 'total_amount', align: 'right'}
{width: colPtsExp[0], header: headerParts[0] || 'Expense', field: 'title', align: 'left'},
{width: colPtsExp[1], header: headerParts[1] || 'Date', field: 'expense_date', align: 'center'},
{width: colPtsExp[2], header: headerParts[2] || 'Category', field: 'category', align: 'left'},
{width: colPtsExp[3], header: headerParts[3] || 'Amount', field: 'total_amount', align: 'right'}
],
data: expensesData,
row_template: expensesRowTemplate,
style: {
headerBackground: '#fff3cd',
headerTextColor: '#856404',
rowBackground: '#fffbf0',
rowTextColor: '#856404'
headerBackground: defExpHeaderBg,
headerTextColor: expHeaderTextHex,
rowBackground: defExpRowBg,
rowTextColor: expItemsTextHex,
borderColor: expLineStrokeHex,
borderWidth: expLineStrokeW
}
});
// Legacy HTML for preview
bodyContent += ` <!-- Expenses Table Start -->\n`;
bodyContent += ` <div style="position:absolute;left:${x}px;top:${y}px;opacity:${opacity};width:515px;">\n`;
bodyContent += ` <table style="width:100%;border-collapse:collapse;font-size:11px;background:#fffbf0;">\n`;
bodyContent += ` <div style="position:absolute;left:${x}px;top:${y}px;opacity:${opacity};width:${tableWidthPxExp}px;">\n`;
bodyContent += ` <table style="width:100%;border-collapse:collapse;font-size:11px;background:${defExpRowBg};">\n`;
bodyContent += ` <thead>\n`;
bodyContent += ` <tr style="background-color:#fff3cd;border-bottom:2px solid #856404;">\n`;
bodyContent += ` <th style="text-align:left;padding:10px;font-weight:bold;font-size:12px;color:#856404;">${(headerParts[0] || 'Expense').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:center;padding:10px;font-weight:bold;font-size:12px;width:100px;color:#856404;">${(headerParts[1] || 'Date').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:left;padding:10px;font-weight:bold;font-size:12px;width:110px;color:#856404;">${(headerParts[2] || 'Category').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:right;padding:10px;font-weight:bold;font-size:12px;width:110px;color:#856404;">${(headerParts[3] || 'Amount').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <tr style="background-color:${defExpHeaderBg};border-bottom:${expLineStrokeW}px solid ${expLineStrokeHex};">\n`;
bodyContent += ` <th style="text-align:left;padding:10px;font-weight:bold;font-size:12px;color:${expHeaderTextHex};">${(headerParts[0] || 'Expense').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:center;padding:10px;font-weight:bold;font-size:12px;width:100px;color:${expHeaderTextHex};">${(headerParts[1] || 'Date').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:left;padding:10px;font-weight:bold;font-size:12px;width:110px;color:${expHeaderTextHex};">${(headerParts[2] || 'Category').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` <th style="text-align:right;padding:10px;font-weight:bold;font-size:12px;width:110px;color:${expHeaderTextHex};">${(headerParts[3] || 'Amount').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</th>\n`;
bodyContent += ` </tr>\n`;
bodyContent += ` </thead>\n`;
bodyContent += ` <tbody>\n`;
{% raw %}
bodyContent += ` {% if invoice.expenses %}\n`;
bodyContent += ` {% for expense in invoice.expenses %}\n`;
bodyContent += ` <tr style="border-bottom:1px solid #f0e5c1;">\n`;
bodyContent += ` <td style="padding:10px;vertical-align:top;color:#856404;">{{ expense.title }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:center;vertical-align:top;color:#856404;">{{ expense.expense_date }}</td>\n`;
bodyContent += ` <td style="padding:10px;vertical-align:top;color:#856404;">{{ expense.category }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:right;vertical-align:top;font-weight:bold;color:#856404;">{{ format_money(expense.total_amount) }}</td>\n`;
bodyContent += ` <tr style="border-bottom:${expLineStrokeW}px solid #f0e5c1;">\n`;
bodyContent += ` <td style="padding:10px;vertical-align:top;color:${expItemsTextHex};">{{ expense.title }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:center;vertical-align:top;color:${expItemsTextHex};">{{ expense.expense_date }}</td>\n`;
bodyContent += ` <td style="padding:10px;vertical-align:top;color:${expItemsTextHex};">{{ expense.category }}</td>\n`;
bodyContent += ` <td style="padding:10px;text-align:right;vertical-align:top;font-weight:bold;color:${expItemsTextHex};">{{ format_money(expense.total_amount) }}</td>\n`;
bodyContent += ` </tr>\n`;
bodyContent += ` {% endfor %}\n`;
bodyContent += ` {% else %}\n`;
+81 -18
View File
@@ -363,13 +363,14 @@ class ReportLabTemplateRenderer:
# Tables are rendered as flowables (they flow naturally)
# Add spacer to position table vertically based on y coordinate
table_y = element.get("y", 0)
page_config = self.template.get("page", {})
margins = page_config.get("margin", {"top": 20, "right": 20, "bottom": 20, "left": 20})
margin_top = margins.get("top", 20) * mm
margin_left = margins.get("left", 20) * mm
if table_y > 0:
# Tables are flowables, so they start from the top margin
# The y position in the template is from top of page (0,0 = top-left)
# We need to position the table relative to the top margin
page_config = self.template.get("page", {})
margins = page_config.get("margin", {"top": 20, "right": 20, "bottom": 20, "left": 20})
margin_top = margins.get("top", 20) * mm
# table_y is absolute from top of page, so we need to account for the margin
# The spacer should position the table at the correct y position
spacer_height = max(0, table_y - margin_top)
@@ -378,12 +379,36 @@ class ReportLabTemplateRenderer:
table_flowable = self._render_table(element)
if table_flowable:
# Wrap table to respect width if specified
table_width = element.get("width")
if table_width:
# Table width is already set in _render_table via colWidths
pass
story.append(table_flowable)
col_widths = self._scaled_table_col_widths(element)
total_table_w = sum(col_widths) if col_widths else 0.0
table_x = element.get("x") or 0
try:
table_x_f = float(table_x)
except (TypeError, ValueError):
table_x_f = 0.0
try:
margin_left_f = float(margin_left)
except (TypeError, ValueError):
margin_left_f = 0.0
left_offset = max(0.0, table_x_f - margin_left_f)
if left_offset > 0.01 and total_table_w > 0.01:
spacer_para = Paragraph("", self._get_style({"style": {}}, "NormalText"))
outer = Table([[spacer_para, table_flowable]], colWidths=[left_offset, total_table_w])
outer.setStyle(
TableStyle(
[
("LEFTPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
("TOPPADDING", (0, 0), (-1, -1), 0),
("BOTTOMPADDING", (0, 0), (-1, -1), 0),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]
)
)
outer.hAlign = "LEFT"
story.append(outer)
else:
story.append(table_flowable)
else:
# For absolute positioning, store element data to draw in page callback
element_data = {"element": element, "renderer": self}
@@ -665,6 +690,41 @@ class ReportLabTemplateRenderer:
total_amount=amt,
)
def _scaled_table_col_widths(self, element: Dict[str, Any]) -> List[float]:
"""Column widths in points, scaled so their sum matches element.width when set."""
columns = element.get("columns") or []
if not columns:
return []
raw: List[float] = []
for c in columns:
try:
raw.append(float(c.get("width", 100) or 100))
except (TypeError, ValueError):
raw.append(100.0)
s = sum(raw)
if s <= 0:
raw = [100.0] * len(columns)
s = sum(raw)
target_f: Optional[float] = None
tw = element.get("width")
if tw is not None:
try:
twf = float(tw)
if twf > 0:
target_f = twf
except (TypeError, ValueError):
target_f = None
if target_f is not None and abs(s - target_f) > 0.01:
scale = target_f / s
raw = [w * scale for w in raw]
min_w = 10.0
raw = [max(min_w, w) for w in raw]
s2 = sum(raw)
if target_f is not None and s2 > target_f + 0.01:
scale2 = target_f / s2
raw = [w * scale2 for w in raw]
return raw
def _render_table(self, element: Dict[str, Any]) -> Table:
"""Render a table element"""
columns = element.get("columns", [])
@@ -732,14 +792,8 @@ class ReportLabTemplateRenderer:
empty_row.extend([Paragraph("", self._get_style({"style": {}}, "NormalText"))] * (num_cols - 1))
table_data.append(empty_row)
# Calculate column widths (convert from points to ReportLab units)
# Note: columns already have width in points from generateCode
col_widths = []
total_width = 0
for col in columns:
col_width = col.get("width", 100) # Already in points
col_widths.append(col_width)
total_width += col_width
# Column widths in points (scaled to element.width when set)
col_widths = self._scaled_table_col_widths(element)
# Create table with proper column widths
table = Table(table_data, colWidths=col_widths, repeatRows=1)
@@ -761,6 +815,15 @@ class ReportLabTemplateRenderer:
row_bg = colors.HexColor(style_config.get("rowBackground", "#ffffff"))
row_text_color = colors.HexColor(style_config.get("rowTextColor", "#000000"))
try:
grid_w = float(style_config.get("borderWidth", 0.5))
except (TypeError, ValueError):
grid_w = 0.5
if grid_w <= 0:
grid_w = 0.5
grid_color_hex = _normalize_color(style_config.get("borderColor", "#e2e8f0")) or "#e2e8f0"
grid_color = colors.HexColor(grid_color_hex)
commands = [
# Header row styling
("BACKGROUND", (0, 0), (-1, 0), header_bg),
@@ -776,7 +839,7 @@ class ReportLabTemplateRenderer:
("FONTSIZE", (0, 1), (-1, -1), 10),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.HexColor("#ffffff"), colors.HexColor("#f9fafb")]),
# Grid and borders
("GRID", (0, 0), (-1, -1), 0.5, colors.HexColor("#e2e8f0")),
("GRID", (0, 0), (-1, -1), grid_w, grid_color),
]
# Apply column alignments (both header and data rows)
+1 -1
View File
@@ -7,7 +7,7 @@ from setuptools import setup, find_packages
setup(
name='timetracker',
version='5.5.6',
version='5.5.7',
packages=find_packages(),
include_package_data=True,
package_data={