|
|
|
|
@ -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")) |
|
|
|
|
|
|
|
|
|
|