|
|
|
|
@ -3811,28 +3811,51 @@ def generate_next_invoice_number(cursor, prefix="INV"):
|
|
|
|
|
next_num += 1 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/invoices/new", methods=["GET", "POST"]) |
|
|
|
|
def new_invoice(): |
|
|
|
|
gate = admin_required() |
|
|
|
|
if gate: |
|
|
|
|
return gate |
|
|
|
|
|
|
|
|
|
ensure_invoice_quote_columns() |
|
|
|
|
conn = get_db_connection() |
|
|
|
|
cursor = conn.cursor(dictionary=True) |
|
|
|
|
|
|
|
|
|
def _load_invoice_form_lists(): |
|
|
|
|
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name") |
|
|
|
|
clients = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name") |
|
|
|
|
services = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
return clients, services |
|
|
|
|
|
|
|
|
|
if request.method == "POST": |
|
|
|
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP |
|
|
|
|
|
|
|
|
|
client_id = request.form.get("client_id", "").strip() |
|
|
|
|
service_id = request.form.get("service_id", "").strip() |
|
|
|
|
currency_code = request.form.get("currency_code", "").strip() |
|
|
|
|
quantity_raw = request.form.get("quantity", "1").strip() |
|
|
|
|
unit_amount_raw = request.form.get("unit_amount", "").strip() |
|
|
|
|
currency_code = request.form.get("currency_code", "").strip() or "CAD" |
|
|
|
|
due_at = request.form.get("due_at", "").strip() |
|
|
|
|
line_description = request.form.get("description", "").strip() |
|
|
|
|
apply_tax = request.form.get("apply_tax") == "1" |
|
|
|
|
|
|
|
|
|
descriptions = request.form.getlist("item_description[]") |
|
|
|
|
quantities = request.form.getlist("item_quantity[]") |
|
|
|
|
unit_amounts = request.form.getlist("item_unit_amount[]") |
|
|
|
|
taxables = request.form.getlist("item_taxable[]") |
|
|
|
|
|
|
|
|
|
max_len = max(len(descriptions), len(quantities), len(unit_amounts), len(taxables), 1) |
|
|
|
|
while len(descriptions) < max_len: |
|
|
|
|
descriptions.append("") |
|
|
|
|
while len(quantities) < max_len: |
|
|
|
|
quantities.append("") |
|
|
|
|
while len(unit_amounts) < max_len: |
|
|
|
|
unit_amounts.append("") |
|
|
|
|
while len(taxables) < max_len: |
|
|
|
|
taxables.append("0") |
|
|
|
|
|
|
|
|
|
errors = [] |
|
|
|
|
line_items = [] |
|
|
|
|
|
|
|
|
|
if not client_id: |
|
|
|
|
errors.append("Client is required.") |
|
|
|
|
@ -3840,68 +3863,92 @@ def new_invoice():
|
|
|
|
|
errors.append("Service is required.") |
|
|
|
|
if not currency_code: |
|
|
|
|
errors.append("Currency is required.") |
|
|
|
|
if not quantity_raw: |
|
|
|
|
errors.append("Quantity is required.") |
|
|
|
|
if not unit_amount_raw: |
|
|
|
|
errors.append("Unit cost is required.") |
|
|
|
|
if not due_at: |
|
|
|
|
errors.append("Due date is required.") |
|
|
|
|
if not line_description: |
|
|
|
|
errors.append("Description is required.") |
|
|
|
|
|
|
|
|
|
quantity = None |
|
|
|
|
unit_amount = None |
|
|
|
|
subtotal_amount = None |
|
|
|
|
subtotal_amount = Decimal("0.00000000") |
|
|
|
|
tax_amount = Decimal("0.00000000") |
|
|
|
|
total_amount = None |
|
|
|
|
|
|
|
|
|
if not errors: |
|
|
|
|
try: |
|
|
|
|
quantity = Decimal(quantity_raw).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) |
|
|
|
|
if quantity <= 0: |
|
|
|
|
errors.append("Quantity must be greater than zero.") |
|
|
|
|
except (InvalidOperation, ValueError): |
|
|
|
|
errors.append("Quantity must be a valid number.") |
|
|
|
|
for idx in range(max_len): |
|
|
|
|
desc = (descriptions[idx] or "").strip() |
|
|
|
|
qty_raw = (quantities[idx] or "").strip() |
|
|
|
|
unit_raw = (unit_amounts[idx] or "").strip() |
|
|
|
|
taxable = str(taxables[idx] or "0").strip() == "1" |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
unit_amount = Decimal(unit_amount_raw).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
if unit_amount <= 0: |
|
|
|
|
errors.append("Unit cost must be greater than zero.") |
|
|
|
|
except (InvalidOperation, ValueError): |
|
|
|
|
errors.append("Unit cost must be a valid number.") |
|
|
|
|
if not desc and not qty_raw and not unit_raw: |
|
|
|
|
continue |
|
|
|
|
|
|
|
|
|
if not errors: |
|
|
|
|
subtotal_amount = (quantity * unit_amount).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
if apply_tax: |
|
|
|
|
tax_amount = (subtotal_amount * Decimal("0.13")).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
total_amount = (subtotal_amount + tax_amount).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
line_no = len(line_items) + 1 |
|
|
|
|
|
|
|
|
|
if errors: |
|
|
|
|
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name") |
|
|
|
|
clients = cursor.fetchall() |
|
|
|
|
if not desc: |
|
|
|
|
errors.append(f"Line {line_no}: description is required.") |
|
|
|
|
if not qty_raw: |
|
|
|
|
errors.append(f"Line {line_no}: quantity is required.") |
|
|
|
|
if not unit_raw: |
|
|
|
|
errors.append(f"Line {line_no}: unit cost is required.") |
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name") |
|
|
|
|
services = cursor.fetchall() |
|
|
|
|
quantity = None |
|
|
|
|
unit_amount = None |
|
|
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
if qty_raw: |
|
|
|
|
try: |
|
|
|
|
quantity = Decimal(qty_raw).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP) |
|
|
|
|
if quantity <= 0: |
|
|
|
|
errors.append(f"Line {line_no}: quantity must be greater than zero.") |
|
|
|
|
except (InvalidOperation, ValueError): |
|
|
|
|
errors.append(f"Line {line_no}: quantity must be a valid number.") |
|
|
|
|
|
|
|
|
|
form_data = { |
|
|
|
|
"client_id": client_id, |
|
|
|
|
"service_id": service_id, |
|
|
|
|
"currency_code": currency_code, |
|
|
|
|
"quantity": quantity_raw, |
|
|
|
|
"unit_amount": unit_amount_raw, |
|
|
|
|
"due_at": due_at, |
|
|
|
|
"description": line_description, |
|
|
|
|
"apply_tax": "1" if apply_tax else "", |
|
|
|
|
} |
|
|
|
|
if unit_raw: |
|
|
|
|
try: |
|
|
|
|
unit_amount = Decimal(unit_raw).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
if unit_amount <= 0: |
|
|
|
|
errors.append(f"Line {line_no}: unit cost must be greater than zero.") |
|
|
|
|
except (InvalidOperation, ValueError): |
|
|
|
|
errors.append(f"Line {line_no}: unit cost must be a valid number.") |
|
|
|
|
|
|
|
|
|
line_total = Decimal("0.00000000") |
|
|
|
|
line_tax = Decimal("0.00000000") |
|
|
|
|
|
|
|
|
|
if quantity is not None and unit_amount is not None: |
|
|
|
|
line_total = (quantity * unit_amount).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
if taxable: |
|
|
|
|
line_tax = (line_total * Decimal("0.13")).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
|
|
|
|
|
line_items.append({ |
|
|
|
|
"description": desc, |
|
|
|
|
"quantity": str(quantity) if quantity is not None else qty_raw, |
|
|
|
|
"unit_amount": str(unit_amount) if unit_amount is not None else unit_raw, |
|
|
|
|
"line_total": str(line_total), |
|
|
|
|
"taxable": "1" if taxable else "0", |
|
|
|
|
"line_tax": str(line_tax), |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
subtotal_amount += line_total |
|
|
|
|
tax_amount += line_tax |
|
|
|
|
|
|
|
|
|
if not line_items: |
|
|
|
|
errors.append("At least one invoice line is required.") |
|
|
|
|
|
|
|
|
|
subtotal_amount = subtotal_amount.quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
tax_amount = tax_amount.quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
total_amount = (subtotal_amount + tax_amount).quantize(Decimal("0.00000001"), rounding=ROUND_HALF_UP) |
|
|
|
|
|
|
|
|
|
if errors: |
|
|
|
|
clients, services = _load_invoice_form_lists() |
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
return render_template( |
|
|
|
|
"invoices/new.html", |
|
|
|
|
clients=clients, |
|
|
|
|
services=services, |
|
|
|
|
errors=errors, |
|
|
|
|
form_data=form_data, |
|
|
|
|
form_data={ |
|
|
|
|
"client_id": client_id, |
|
|
|
|
"service_id": service_id, |
|
|
|
|
"currency_code": currency_code, |
|
|
|
|
"due_at": due_at, |
|
|
|
|
"line_items": line_items, |
|
|
|
|
}, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
oracle_snapshot = fetch_oracle_quote_snapshot(currency_code, str(total_amount)) |
|
|
|
|
@ -3912,6 +3959,7 @@ def new_invoice():
|
|
|
|
|
|
|
|
|
|
insert_cursor = conn.cursor() |
|
|
|
|
invoice_number = generate_next_invoice_number(insert_cursor) |
|
|
|
|
notes = "\n".join([item["description"] for item in line_items]).strip() |
|
|
|
|
|
|
|
|
|
insert_cursor.execute(""" |
|
|
|
|
INSERT INTO invoices |
|
|
|
|
@ -3942,7 +3990,7 @@ def new_invoice():
|
|
|
|
|
subtotal_amount, |
|
|
|
|
tax_amount, |
|
|
|
|
due_at, |
|
|
|
|
line_description, |
|
|
|
|
notes, |
|
|
|
|
quote_fiat_amount, |
|
|
|
|
quote_fiat_currency, |
|
|
|
|
quote_expires_at, |
|
|
|
|
@ -3951,41 +3999,38 @@ def new_invoice():
|
|
|
|
|
|
|
|
|
|
invoice_id = insert_cursor.lastrowid |
|
|
|
|
|
|
|
|
|
insert_cursor.execute(""" |
|
|
|
|
INSERT INTO invoice_items |
|
|
|
|
( |
|
|
|
|
for line_number, item in enumerate(line_items, start=1): |
|
|
|
|
insert_cursor.execute(""" |
|
|
|
|
INSERT INTO invoice_items |
|
|
|
|
( |
|
|
|
|
invoice_id, |
|
|
|
|
line_number, |
|
|
|
|
item_type, |
|
|
|
|
description, |
|
|
|
|
quantity, |
|
|
|
|
unit_amount, |
|
|
|
|
line_total, |
|
|
|
|
currency_code, |
|
|
|
|
service_id |
|
|
|
|
) |
|
|
|
|
VALUES (%s, %s, 'service', %s, %s, %s, %s, %s, %s) |
|
|
|
|
""", ( |
|
|
|
|
invoice_id, |
|
|
|
|
line_number, |
|
|
|
|
item_type, |
|
|
|
|
description, |
|
|
|
|
quantity, |
|
|
|
|
unit_amount, |
|
|
|
|
line_total, |
|
|
|
|
item["description"], |
|
|
|
|
item["quantity"], |
|
|
|
|
item["unit_amount"], |
|
|
|
|
item["line_total"], |
|
|
|
|
currency_code, |
|
|
|
|
service_id |
|
|
|
|
) |
|
|
|
|
VALUES (%s, 1, 'service', %s, %s, %s, %s, %s, %s) |
|
|
|
|
""", ( |
|
|
|
|
invoice_id, |
|
|
|
|
line_description, |
|
|
|
|
quantity, |
|
|
|
|
unit_amount, |
|
|
|
|
subtotal_amount, |
|
|
|
|
currency_code, |
|
|
|
|
service_id |
|
|
|
|
)) |
|
|
|
|
)) |
|
|
|
|
|
|
|
|
|
conn.commit() |
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
return redirect("/invoices") |
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, client_code, company_name FROM clients ORDER BY company_name") |
|
|
|
|
clients = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
cursor.execute("SELECT id, service_code, service_name FROM services ORDER BY service_name") |
|
|
|
|
services = cursor.fetchall() |
|
|
|
|
return redirect(f"/invoices/view/{invoice_id}") |
|
|
|
|
|
|
|
|
|
clients, services = _load_invoice_form_lists() |
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
return render_template( |
|
|
|
|
@ -3993,12 +4038,15 @@ def new_invoice():
|
|
|
|
|
clients=clients, |
|
|
|
|
services=services, |
|
|
|
|
errors=[], |
|
|
|
|
form_data={}, |
|
|
|
|
form_data={ |
|
|
|
|
"currency_code": "CAD", |
|
|
|
|
"line_items": [ |
|
|
|
|
{"description": "", "quantity": "1", "unit_amount": "", "taxable": "0"} |
|
|
|
|
], |
|
|
|
|
}, |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/invoices/pdf/<int:invoice_id>") |
|
|
|
|
def invoice_pdf(invoice_id, client_copy=False): |
|
|
|
|
conn = get_db_connection() |
|
|
|
|
@ -4025,6 +4073,24 @@ def invoice_pdf(invoice_id, client_copy=False):
|
|
|
|
|
conn.close() |
|
|
|
|
return "Invoice not found", 404 |
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
SELECT line_number, description, quantity, unit_amount, line_total, currency_code |
|
|
|
|
FROM invoice_items |
|
|
|
|
WHERE invoice_id = %s |
|
|
|
|
ORDER BY line_number ASC, id ASC |
|
|
|
|
""", (invoice_id,)) |
|
|
|
|
invoice_items = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
if not invoice_items: |
|
|
|
|
invoice_items = [{ |
|
|
|
|
"line_number": 1, |
|
|
|
|
"description": invoice.get("notes") or "-", |
|
|
|
|
"quantity": 1, |
|
|
|
|
"unit_amount": invoice.get("subtotal_amount"), |
|
|
|
|
"line_total": invoice.get("subtotal_amount"), |
|
|
|
|
"currency_code": invoice.get("currency_code", "CAD"), |
|
|
|
|
}] |
|
|
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
settings = get_app_settings() |
|
|
|
|
@ -4170,33 +4236,37 @@ def invoice_pdf(invoice_id, client_copy=False):
|
|
|
|
|
y -= 16 |
|
|
|
|
|
|
|
|
|
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 item in invoice_items: |
|
|
|
|
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(item.get("description") or "-", 265, "Helvetica", 10) |
|
|
|
|
row_count = max(len(service_code_lines), len(service_lines), len(description_lines), 1) |
|
|
|
|
|
|
|
|
|
for idx in range(row_count): |
|
|
|
|
if y < 120: |
|
|
|
|
pdf.showPage() |
|
|
|
|
y = height - 50 |
|
|
|
|
line_amount = item.get("line_total") |
|
|
|
|
if line_amount is None: |
|
|
|
|
line_amount = invoice.get("subtotal_amount") |
|
|
|
|
|
|
|
|
|
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"))) |
|
|
|
|
for idx in range(row_count): |
|
|
|
|
if y < 120: |
|
|
|
|
pdf.showPage() |
|
|
|
|
y = height - 50 |
|
|
|
|
|
|
|
|
|
y -= 13 |
|
|
|
|
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 -= 15 |
|
|
|
|
y -= 13 |
|
|
|
|
|
|
|
|
|
y -= 6 |
|
|
|
|
|
|
|
|
|
y -= 9 |
|
|
|
|
|
|
|
|
|
totals_x_label = 360 |
|
|
|
|
totals_x_value = right |
|
|
|
|
@ -4380,6 +4450,24 @@ def view_invoice(invoice_id):
|
|
|
|
|
conn.close() |
|
|
|
|
return "Invoice not found", 404 |
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
SELECT line_number, description, quantity, unit_amount, line_total, currency_code |
|
|
|
|
FROM invoice_items |
|
|
|
|
WHERE invoice_id = %s |
|
|
|
|
ORDER BY line_number ASC, id ASC |
|
|
|
|
""", (invoice_id,)) |
|
|
|
|
invoice_items = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
if not invoice_items: |
|
|
|
|
invoice_items = [{ |
|
|
|
|
"line_number": 1, |
|
|
|
|
"description": invoice.get("notes") or "-", |
|
|
|
|
"quantity": 1, |
|
|
|
|
"unit_amount": invoice.get("subtotal_amount"), |
|
|
|
|
"line_total": invoice.get("subtotal_amount"), |
|
|
|
|
"currency_code": invoice.get("currency_code", "CAD"), |
|
|
|
|
}] |
|
|
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
settings = get_app_settings() |
|
|
|
|
invoice_payments = get_invoice_payments(invoice_id) |
|
|
|
|
@ -4387,7 +4475,8 @@ def view_invoice(invoice_id):
|
|
|
|
|
"invoices/view.html", |
|
|
|
|
invoice=invoice, |
|
|
|
|
settings=settings, |
|
|
|
|
invoice_payments=invoice_payments |
|
|
|
|
invoice_payments=invoice_payments, |
|
|
|
|
invoice_items=invoice_items |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -5538,6 +5627,24 @@ def portal_invoice_pdf(invoice_id):
|
|
|
|
|
conn.close() |
|
|
|
|
return redirect("/portal/dashboard") |
|
|
|
|
|
|
|
|
|
cursor.execute(""" |
|
|
|
|
SELECT line_number, description, quantity, unit_amount, line_total, currency_code |
|
|
|
|
FROM invoice_items |
|
|
|
|
WHERE invoice_id = %s |
|
|
|
|
ORDER BY line_number ASC, id ASC |
|
|
|
|
""", (invoice_id,)) |
|
|
|
|
invoice_items = cursor.fetchall() |
|
|
|
|
|
|
|
|
|
if not invoice_items: |
|
|
|
|
invoice_items = [{ |
|
|
|
|
"line_number": 1, |
|
|
|
|
"description": invoice.get("notes") or "-", |
|
|
|
|
"quantity": 1, |
|
|
|
|
"unit_amount": invoice.get("subtotal_amount"), |
|
|
|
|
"line_total": invoice.get("subtotal_amount"), |
|
|
|
|
"currency_code": invoice.get("currency_code", "CAD"), |
|
|
|
|
}] |
|
|
|
|
|
|
|
|
|
conn.close() |
|
|
|
|
|
|
|
|
|
settings = get_app_settings() |
|
|
|
|
@ -5682,33 +5789,37 @@ def portal_invoice_pdf(invoice_id):
|
|
|
|
|
y -= 16 |
|
|
|
|
|
|
|
|
|
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 item in invoice_items: |
|
|
|
|
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(item.get("description") or "-", 265, "Helvetica", 10) |
|
|
|
|
row_count = max(len(service_code_lines), len(service_lines), len(description_lines), 1) |
|
|
|
|
|
|
|
|
|
for idx in range(row_count): |
|
|
|
|
if y < 120: |
|
|
|
|
pdf.showPage() |
|
|
|
|
y = height - 50 |
|
|
|
|
line_amount = item.get("line_total") |
|
|
|
|
if line_amount is None: |
|
|
|
|
line_amount = invoice.get("subtotal_amount") |
|
|
|
|
|
|
|
|
|
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"))) |
|
|
|
|
for idx in range(row_count): |
|
|
|
|
if y < 120: |
|
|
|
|
pdf.showPage() |
|
|
|
|
y = height - 50 |
|
|
|
|
|
|
|
|
|
y -= 13 |
|
|
|
|
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 -= 15 |
|
|
|
|
y -= 13 |
|
|
|
|
|
|
|
|
|
y -= 6 |
|
|
|
|
|
|
|
|
|
y -= 9 |
|
|
|
|
|
|
|
|
|
totals_x_label = 360 |
|
|
|
|
totals_x_value = right |
|
|
|
|
|