Files
TimeTracker/migrations/fix_invoice_pdf_template_items_source.py
T
Dries Peeters 5bc637cc6b fix(invoices): include extra goods and expenses in PDF invoice exports
PDF invoices were missing extra goods (and expenses) because the ReportLab
template renderer only used invoice.items as the table data source.

- Add invoice.all_line_items to template context: merged list of items,
  extra_goods, and expenses with normalized description/quantity/price fields
- Update default template schema to use invoice.all_line_items instead of
  invoice.items for the items table
- Add migration to update existing saved templates with the new data source
- Update PDF layout designer: add all_line_items and extra_goods loop options,
  default items table to all_line_items
- Add expenses to fallback ReportLab generator for consistency with
  pdf_default.html

Fixes #503

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-07 22:24:45 +01:00

86 lines
2.9 KiB
Python

#!/usr/bin/env python3
"""
Migration script to fix invoice PDF template table data source.
Updates table elements from invoice.items to invoice.all_line_items so that
extra goods and expenses are included in PDF exports (fixes Issue #503).
"""
import sys
import os
import json
# Add parent directory to path to import app
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app import create_app, db
from app.models import InvoicePDFTemplate
OLD_DATA_SOURCE = "{{ invoice.items }}"
NEW_DATA_SOURCE = "{{ invoice.all_line_items }}"
def fix_invoice_pdf_templates():
"""Update invoice PDF templates to use all_line_items instead of items"""
app = create_app()
with app.app_context():
templates = InvoicePDFTemplate.query.all()
if not templates:
print("No invoice PDF templates found in database.")
return
print(f"Found {len(templates)} invoice PDF template(s) to process.")
updated_count = 0
for template in templates:
if not template.template_json or not template.template_json.strip():
continue
try:
data = json.loads(template.template_json)
except json.JSONDecodeError as e:
print(f" Skipping template {template.page_size} (id={template.id}): invalid JSON - {e}")
continue
elements = data.get("elements", [])
modified = False
for element in elements:
if element.get("type") == "table":
data_src = element.get("data", "")
# Handle exact match and variations with extra whitespace
if data_src.strip() == OLD_DATA_SOURCE.strip():
element["data"] = NEW_DATA_SOURCE
modified = True
print(f" Updating template {template.page_size}: table data source")
if modified:
template.template_json = json.dumps(data)
updated_count += 1
if updated_count > 0:
try:
db.session.commit()
print(f"\nSuccessfully updated {updated_count} invoice PDF template(s).")
print("PDF exports will now include items, extra goods, and expenses.")
except Exception as e:
db.session.rollback()
print(f"Error updating templates: {e}")
sys.exit(1)
else:
print("\nNo templates required updates. All templates already use the new data source.")
if __name__ == "__main__":
print("=" * 60)
print("Invoice PDF Template Migration (Issue #503)")
print("=" * 60)
print("\nThis migration updates invoice PDF templates to include")
print("extra goods and expenses in the items table.")
print("\nChange: invoice.items -> invoice.all_line_items")
print()
fix_invoice_pdf_templates()