']
-
+
elements = template_json.get("elements", [])
for idx, element in enumerate(elements):
elem_type = element.get("type", "")
@@ -269,36 +284,36 @@ def replace_var(match):
style = element.get("style", {})
opacity = style.get("opacity", 1.0)
rotation = element.get("rotation", 0)
-
+
# Convert points to pixels at 96 DPI for browser
x_px = int(x * 96 / 72)
y_px = int(y * 96 / 72)
-
+
# Base style string
base_style_parts = [
f"left: {x_px}px",
f"top: {y_px}px",
f"opacity: {opacity}",
]
-
+
if rotation:
base_style_parts.append(f"transform: rotate({rotation}deg)")
base_style_parts.append("transform-origin: top left")
-
+
style_str_base = "; ".join(base_style_parts)
-
+
if elem_type == "text":
text = element.get("text", "")
width = element.get("width", 400)
height = element.get("height", None)
width_px_elem = int(width * 96 / 72)
-
+
font_name = style.get("font", "Helvetica")
font_size = style.get("size", 10)
color = format_color(style.get("color", "#000000"))
align = style.get("align", "left")
valign = style.get("valign", "top")
-
+
# Build complete style string
style_parts = [style_str_base]
style_parts.append(f"width: {width_px_elem}px")
@@ -311,20 +326,20 @@ def replace_var(match):
style_parts.append(f"color: {color}")
style_parts.append(f"text-align: {align}")
style_parts.append(f"vertical-align: {valign}")
-
+
style_str = "; ".join(style_parts) + ";"
-
+
# Render text with actual data if available
data_obj = invoice if invoice else quote
rendered_text = render_text_template(text, data_obj, settings) if (data_obj or settings) else text
-
+
# Escape HTML but preserve any remaining template syntax
text_escaped = html_escape.escape(rendered_text)
# Restore template syntax if any remains (shouldn't after rendering, but just in case)
text_escaped = text_escaped.replace("<{{", "{{").replace("}}>", "}}")
-
+
html_parts.append(f'
{text_escaped}
')
-
+
elif elem_type == "image":
width = element.get("width", 100)
height = element.get("height", 100)
@@ -332,7 +347,7 @@ def replace_var(match):
height_px_elem = int(height * 96 / 72)
source = element.get("source", "")
is_decorative = element.get("decorative", False)
-
+
# Handle base64 data URLs or file paths
img_src = ""
if source.startswith("data:"):
@@ -341,6 +356,7 @@ def replace_var(match):
# Template image - convert to base64 for PDF generation
try:
from app.utils.template_filters import get_image_base64
+
# Extract filename from URL
filename = source.split("/uploads/template_images/")[-1]
# Build file path relative to app root (as get_image_base64 expects)
@@ -360,18 +376,22 @@ def replace_var(match):
else:
# Placeholder for decorative images without source
img_src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Crect fill='%23ddd' width='100' height='100'/%3E%3Ctext x='50%25' y='50%25' text-anchor='middle' dy='.3em' fill='%23999'%3EImage%3C/text%3E%3C/svg%3E"
-
+
style_parts = [style_str_base]
style_parts.append(f"width: {width_px_elem}px")
style_parts.append(f"height: {height_px_elem}px")
style_str = "; ".join(style_parts) + ";"
-
+
if img_src and not img_src.startswith("data:image/svg+xml"):
- html_parts.append(f'

')
+ html_parts.append(
+ f'

'
+ )
else:
# Show placeholder for decorative images without source
- html_parts.append(f'
Decorative Image
')
-
+ html_parts.append(
+ f'
Decorative Image
'
+ )
+
elif elem_type == "rectangle":
width = element.get("width", 100)
height = element.get("height", 100)
@@ -380,7 +400,7 @@ def replace_var(match):
fill = format_color(style.get("fill", "#ffffff"))
stroke = format_color(style.get("stroke", "#000000"))
stroke_width = style.get("strokeWidth", 1)
-
+
style_parts = [style_str_base]
style_parts.append(f"width: {width_px_elem}px")
style_parts.append(f"height: {height_px_elem}px")
@@ -388,16 +408,16 @@ def replace_var(match):
if stroke_width > 0:
style_parts.append(f"border: {stroke_width}px solid {stroke}")
style_str = "; ".join(style_parts) + ";"
-
+
html_parts.append(f'
')
-
+
elif elem_type == "circle":
radius = element.get("radius", 50)
radius_px = int(radius * 96 / 72)
fill = format_color(style.get("fill", "#ffffff"))
stroke = format_color(style.get("stroke", "#000000"))
stroke_width = style.get("strokeWidth", 1)
-
+
style_parts = [style_str_base]
style_parts.append(f"width: {radius_px * 2}px")
style_parts.append(f"height: {radius_px * 2}px")
@@ -405,9 +425,9 @@ def replace_var(match):
if stroke_width > 0:
style_parts.append(f"border: {stroke_width}px solid {stroke}")
style_str = "; ".join(style_parts) + ";"
-
+
html_parts.append(f'
')
-
+
elif elem_type == "line":
width = element.get("width", 100)
height = element.get("height", 0)
@@ -416,33 +436,33 @@ def replace_var(match):
stroke_width = height if height > 0 else style.get("strokeWidth", 1)
stroke_width_px = max(1, int(stroke_width * 96 / 72))
stroke = format_color(style.get("stroke", "#000000"))
-
+
style_parts = [style_str_base]
style_parts.append(f"width: {width_px_elem}px")
style_parts.append(f"height: {stroke_width_px}px")
style_parts.append(f"background-color: {stroke}")
style_str = "; ".join(style_parts) + ";"
-
+
html_parts.append(f'
')
-
+
elif elem_type == "table":
width = element.get("width", 500)
width_px_elem = int(width * 96 / 72)
columns = element.get("columns", [])
row_template = element.get("row_template", {})
-
+
# Get table style properties
table_style = element.get("style", {})
border_color = format_color(table_style.get("borderColor", "#000000"))
header_bg = format_color(table_style.get("headerBackground", "#f8f9fa"))
-
+
style_parts = [style_str_base]
style_parts.append(f"width: {width_px_elem}px")
style_parts.append(f"border: 1px solid {border_color}")
style_str = "; ".join(style_parts) + ";"
-
+
table_html = f'
'
-
+
# Build header row
for col in columns:
header = col.get("header", "")
@@ -450,8 +470,8 @@ def replace_var(match):
col_width = col.get("width", None)
width_attr = f' width="{int(col_width * 96 / 72)}px"' if col_width else ""
table_html += f'| {html_escape.escape(header)} | '
- table_html += '
'
-
+ table_html += ""
+
# Resolve table data from element's data source (e.g. invoice.all_line_items or invoice.items)
data_obj = invoice if invoice else quote
items = []
@@ -486,15 +506,15 @@ def replace_var(match):
items = list(data_obj.items) if data_obj.items else []
except Exception:
items = []
-
+
# If no items available, create sample row from template
if not items and row_template:
items = [row_template] # Use template as sample data
-
+
# Render table rows with actual data
if items:
for item in items[:10]: # Limit to 10 rows for preview
- table_html += ''
+ table_html += "
"
for col in columns:
field = col.get("field", "")
align = col.get("align", "left")
@@ -509,26 +529,28 @@ def replace_var(match):
value = ""
except Exception:
value = ""
-
+
value_escaped = html_escape.escape(str(value))
table_html += f'| {value_escaped} | '
- table_html += '
'
+ table_html += ""
else:
# No data available, show template placeholders
- table_html += ''
+ table_html += "
"
for col in columns:
field = col.get("field", "")
align = col.get("align", "left")
placeholder = f"{{{{ {field} }}}}"
- table_html += f'| {html_escape.escape(placeholder)} | '
- table_html += '
'
-
- table_html += '
'
+ table_html += (
+ f'
{html_escape.escape(placeholder)} | '
+ )
+ table_html += ""
+
+ table_html += ""
html_parts.append(table_html)
-
- html_parts.append('