Browse Source

Bump to v2.0.7 PDF invoice description wrapping

main
def 3 weeks ago
parent
commit
19589c40f0
  1. 8
      PROJECT_STATE.md
  2. 8
      README.md
  3. 2
      VERSION
  4. 186
      backend/app.py

8
PROJECT_STATE.md

@ -1,3 +1,11 @@
## v2.0.7 PDF invoice description wrapping - 2026-05-29 UTC
- Fixed invoice PDFs so long invoice descriptions wrap across multiple lines instead of being truncated.
- Updated PDF invoice item line to show subtotal/line amount instead of tax-included total.
- PDF totals now show HST only when tax exists.
- Portal Download All Invoices now creates client-copy PDFs without the optional invoice footer block.
- Kept admin/portal HTML invoice views unchanged.
## v2.0.6 health revenue dashboard - 2026-05-29 UTC
- Added Square / Revenue Health panel to /health.

8
README.md

@ -1,3 +1,11 @@
## v2.0.7 PDF invoice description wrapping - 2026-05-29 UTC
- Fixed invoice PDFs so long invoice descriptions wrap across multiple lines instead of being truncated.
- Updated PDF invoice item line to show subtotal/line amount instead of tax-included total.
- PDF totals now show HST only when tax exists.
- Portal Download All Invoices now creates client-copy PDFs without the optional invoice footer block.
- Kept admin/portal HTML invoice views unchanged.
## v2.0.6 health revenue dashboard - 2026-05-29 UTC
- Added Square / Revenue Health panel to /health.

2
VERSION

@ -1 +1 @@
v2.0.6
v2.0.7

186
backend/app.py

@ -4000,7 +4000,7 @@ def new_invoice():
@app.route("/invoices/pdf/<int:invoice_id>")
def invoice_pdf(invoice_id):
def invoice_pdf(invoice_id, client_copy=False):
conn = get_db_connection()
cursor = conn.cursor(dictionary=True)
@ -4047,6 +4047,45 @@ def invoice_pdf(invoice_id):
def money(value, currency="CAD"):
return f"{to_decimal(value):.2f} {currency}"
def pdf_wrap_text(text, max_width, font="Helvetica", size=10):
text = str(text if text is not None else "-").replace("\r", " ").replace("\n", " ")
words = text.split()
if not words:
return ["-"]
lines = []
current = ""
for word in words:
candidate = (current + " " + word).strip()
if pdf.stringWidth(candidate, font, size) <= max_width:
current = candidate
continue
if current:
lines.append(current)
current = ""
if pdf.stringWidth(word, font, size) <= max_width:
current = word
continue
chunk = ""
for ch in word:
test = chunk + ch
if pdf.stringWidth(test, font, size) <= max_width:
chunk = test
else:
if chunk:
lines.append(chunk)
chunk = ch
current = chunk
if current:
lines.append(current)
return lines or ["-"]
pdf.setTitle(f"Invoice {invoice['invoice_number']}")
logo_url = (settings.get("business_logo_url") or "").strip()
@ -4121,31 +4160,56 @@ def invoice_pdf(invoice_id):
pdf.drawString(left, y, f"Currency: {invoice.get('currency_code', 'CAD')}")
y -= 28
pdf.setFont("Helvetica-Bold", 11)
pdf.setFont("Helvetica-Bold", 10)
pdf.drawString(left, y, "Service Code")
pdf.drawString(180, y, "Service")
pdf.drawString(330, y, "Description")
pdf.drawRightString(right, y, "Total")
pdf.drawString(125, y, "Service")
pdf.drawString(205, y, "Description")
pdf.drawRightString(right, y, "Amount")
y -= 14
pdf.line(left, y, right, y)
y -= 18
y -= 16
pdf.setFont("Helvetica", 11)
pdf.drawString(left, y, str(invoice.get("service_code") or "-"))
pdf.drawString(180, y, str(invoice.get("service_name") or "-"))
pdf.drawString(330, y, str(invoice.get("notes") or "-")[:28])
pdf.drawRightString(right, y, money(invoice.get("total_amount"), invoice.get("currency_code", "CAD")))
y -= 28
pdf.setFont("Helvetica", 10)
service_code_lines = pdf_wrap_text(invoice.get("service_code") or "-", 65, "Helvetica", 10)
service_lines = pdf_wrap_text(invoice.get("service_name") or "-", 70, "Helvetica", 10)
description_lines = pdf_wrap_text(invoice.get("notes") or "-", 265, "Helvetica", 10)
row_count = max(len(service_code_lines), len(service_lines), len(description_lines), 1)
line_amount = invoice.get("subtotal_amount")
if line_amount is None:
line_amount = invoice.get("total_amount")
for idx in range(row_count):
if y < 120:
pdf.showPage()
y = height - 50
pdf.setFont("Helvetica", 10)
if idx < len(service_code_lines):
pdf.drawString(left, y, service_code_lines[idx])
if idx < len(service_lines):
pdf.drawString(125, y, service_lines[idx])
if idx < len(description_lines):
pdf.drawString(205, y, description_lines[idx])
if idx == 0:
pdf.drawRightString(right, y, money(line_amount, invoice.get("currency_code", "CAD")))
y -= 13
y -= 15
totals_x_label = 360
totals_x_value = right
totals = [
("Subtotal", money(invoice.get("subtotal_amount"), invoice.get("currency_code", "CAD"))),
((settings.get("tax_label") or "Tax"), money(invoice.get("tax_amount"), invoice.get("currency_code", "CAD"))),
]
if to_decimal(invoice.get("tax_amount")) > to_decimal("0"):
totals.append(((settings.get("tax_label") or "Tax"), money(invoice.get("tax_amount"), invoice.get("currency_code", "CAD"))))
totals.extend([
("Total", money(invoice.get("total_amount"), invoice.get("currency_code", "CAD"))),
("Paid", money(invoice.get("amount_paid"), invoice.get("currency_code", "CAD"))),
]
])
remaining = to_decimal(invoice.get("total_amount")) - to_decimal(invoice.get("amount_paid"))
@ -4267,7 +4331,7 @@ def invoice_pdf(invoice_id):
y -= 6
if settings.get("invoice_footer"):
if settings.get("invoice_footer") and not client_copy:
y -= 8
pdf.setFont("Helvetica-Bold", 11)
pdf.drawString(left, y, "Footer")
@ -5333,7 +5397,7 @@ def portal_download_all_invoices():
with zipfile.ZipFile(memory_file, "w", zipfile.ZIP_DEFLATED) as zf:
for inv in invoices:
response = invoice_pdf(inv["id"])
response = invoice_pdf(inv["id"], client_copy=True)
response.direct_passthrough = False
pdf_bytes = response.get_data()
@ -5495,6 +5559,45 @@ def portal_invoice_pdf(invoice_id):
def money(value, currency="CAD"):
return f"{to_decimal(value):.2f} {currency}"
def pdf_wrap_text(text, max_width, font="Helvetica", size=10):
text = str(text if text is not None else "-").replace("\r", " ").replace("\n", " ")
words = text.split()
if not words:
return ["-"]
lines = []
current = ""
for word in words:
candidate = (current + " " + word).strip()
if pdf.stringWidth(candidate, font, size) <= max_width:
current = candidate
continue
if current:
lines.append(current)
current = ""
if pdf.stringWidth(word, font, size) <= max_width:
current = word
continue
chunk = ""
for ch in word:
test = chunk + ch
if pdf.stringWidth(test, font, size) <= max_width:
chunk = test
else:
if chunk:
lines.append(chunk)
chunk = ch
current = chunk
if current:
lines.append(current)
return lines or ["-"]
pdf.setTitle(f"Invoice {invoice['invoice_number']}")
logo_url = (settings.get("business_logo_url") or "").strip()
@ -5569,31 +5672,56 @@ def portal_invoice_pdf(invoice_id):
pdf.drawString(left, y, f"Currency: {invoice.get('currency_code', 'CAD')}")
y -= 28
pdf.setFont("Helvetica-Bold", 11)
pdf.setFont("Helvetica-Bold", 10)
pdf.drawString(left, y, "Service Code")
pdf.drawString(180, y, "Service")
pdf.drawString(330, y, "Description")
pdf.drawRightString(right, y, "Total")
pdf.drawString(125, y, "Service")
pdf.drawString(205, y, "Description")
pdf.drawRightString(right, y, "Amount")
y -= 14
pdf.line(left, y, right, y)
y -= 18
y -= 16
pdf.setFont("Helvetica", 11)
pdf.drawString(left, y, str(invoice.get("service_code") or "-"))
pdf.drawString(180, y, str(invoice.get("service_name") or "-"))
pdf.drawString(330, y, str(invoice.get("notes") or "-")[:28])
pdf.drawRightString(right, y, money(invoice.get("total_amount"), invoice.get("currency_code", "CAD")))
y -= 28
pdf.setFont("Helvetica", 10)
service_code_lines = pdf_wrap_text(invoice.get("service_code") or "-", 65, "Helvetica", 10)
service_lines = pdf_wrap_text(invoice.get("service_name") or "-", 70, "Helvetica", 10)
description_lines = pdf_wrap_text(invoice.get("notes") or "-", 265, "Helvetica", 10)
row_count = max(len(service_code_lines), len(service_lines), len(description_lines), 1)
line_amount = invoice.get("subtotal_amount")
if line_amount is None:
line_amount = invoice.get("total_amount")
for idx in range(row_count):
if y < 120:
pdf.showPage()
y = height - 50
pdf.setFont("Helvetica", 10)
if idx < len(service_code_lines):
pdf.drawString(left, y, service_code_lines[idx])
if idx < len(service_lines):
pdf.drawString(125, y, service_lines[idx])
if idx < len(description_lines):
pdf.drawString(205, y, description_lines[idx])
if idx == 0:
pdf.drawRightString(right, y, money(line_amount, invoice.get("currency_code", "CAD")))
y -= 13
y -= 15
totals_x_label = 360
totals_x_value = right
totals = [
("Subtotal", money(invoice.get("subtotal_amount"), invoice.get("currency_code", "CAD"))),
((settings.get("tax_label") or "Tax"), money(invoice.get("tax_amount"), invoice.get("currency_code", "CAD"))),
]
if to_decimal(invoice.get("tax_amount")) > to_decimal("0"):
totals.append(((settings.get("tax_label") or "Tax"), money(invoice.get("tax_amount"), invoice.get("currency_code", "CAD"))))
totals.extend([
("Total", money(invoice.get("total_amount"), invoice.get("currency_code", "CAD"))),
("Paid", money(invoice.get("amount_paid"), invoice.get("currency_code", "CAD"))),
]
])
remaining = to_decimal(invoice.get("total_amount")) - to_decimal(invoice.get("amount_paid"))

Loading…
Cancel
Save