Files
TimeTracker/docs/PDF_LAYOUT_CUSTOMIZATION.md
T
Dries Peeters 35e3694cc1 Fix admin menu and PDF layout; update docs
Admin menu:
- PDF Templates is now a top-level submenu under Admin (same level as System Settings) so it opens without opening System Settings first.
- Remove PDF routes from admin_settings_open so only PDF Templates expands on invoice/quote PDF pages.
- Set pdfDropdown parent to adminDropdown in nested dropdown click handler.

PDF layout (invoice & quote):
- Preserve table groups (Items, Expenses) on save by skipping Groups in cleanup and ensuring table names are set and restored from design_json.
- Add test for saving and reloading layout with tables.

Docs:
- Update PDF layout access path to Admin → PDF Templates → Invoice PDF (and Quote PDF) in PDF_LAYOUT_CUSTOMIZATION.md, PDF_EDITOR_ENHANCED_FEATURES.md, PDF_EDITOR_QUICK_START.md, INVOICE_EXTRA_GOODS_PDF_EXPORT.md.
2026-02-20 09:56:44 +01:00

18 KiB

PDF Layout Customization Guide

Overview

TimeTracker provides a powerful system-wide PDF layout editor that allows administrators to customize the appearance of invoice PDFs. This feature enables you to:

  • Customize the HTML structure of invoice PDFs
  • Apply custom CSS styling
  • Use Jinja2 template variables to display dynamic data
  • Preview changes in real-time
  • Save and reuse custom templates across all invoices

Accessing the PDF Layout Editor

Admin Access Required

To access the PDF layout editor:

  1. Log in as an administrator
  2. In the sidebar, expand Admin and open PDF Templates, then click Invoice PDF
  3. The PDF Layout Editor page will open

URL: /admin/pdf-layout

The PDF Templates submenu appears directly under Admin (same level as System Settings), so you can open it without expanding System Settings.

Required Permission: manage_settings or admin role

Using the PDF Layout Editor

Interface Overview

The PDF Layout Editor is powered by Konva.js and consists of three main sections:

  1. Element Library (Left Sidebar): Drag-and-drop elements organized by category

    • Basic Elements (text, shapes, lines, decorative images)
    • Company Information (logo, name, address, contact details)
    • Invoice Data (numbers, dates, client info, totals)
    • Advanced Elements (QR codes, watermarks, page numbers)
  2. Canvas Workspace (Center): Visual canvas representing your invoice page (A4 size)

    • Click elements from sidebar to add to canvas
    • Drag elements to reposition
    • Resize using transform handles
    • Toolbar with zoom, delete, and alignment tools
  3. Properties Panel (Right Sidebar): Edit properties of selected element

    • Position (X/Y coordinates)
    • Text content and styling (font, size, color)
    • Shape properties (fill, stroke, dimensions)
    • Layer order controls (z-index)
    • Live preview of generated PDF

Editing Workflow

  1. Add Elements: Click elements from left sidebar to add to canvas
  2. Position: Drag elements to desired locations or use X/Y properties
  3. Customize: Select elements and edit properties in right panel
  4. Align: Use toolbar alignment tools for precise positioning
  5. Layer: Manage z-index with layer order controls
  6. Preview: Click "Generate Preview" to see rendered result
  7. Save: Click "Save Design" to apply system-wide
  8. Reset: If needed, click "Reset" to restore defaults

Quick Start

For a beginner-friendly guide, see PDF Editor Quick Start

For comprehensive feature documentation, see Enhanced PDF Editor Features

Available Template Variables

Invoice Variables

{{ invoice.invoice_number }}           # Invoice number (e.g., "INV-2024-001")
{{ invoice.issue_date }}                # Issue date
{{ invoice.due_date }}                  # Due date
{{ invoice.status }}                    # Status (draft, sent, paid, etc.)
{{ invoice.client_name }}               # Client name
{{ invoice.client_email }}              # Client email
{{ invoice.client_address }}            # Client address
{{ invoice.subtotal }}                  # Subtotal amount
{{ invoice.tax_rate }}                  # Tax rate percentage
{{ invoice.tax_amount }}                # Tax amount
{{ invoice.total_amount }}              # Total amount
{{ invoice.notes }}                     # Invoice notes
{{ invoice.terms }}                     # Invoice terms

Project Variables

{{ invoice.project.name }}              # Project name
{{ invoice.project.description }}       # Project description

Invoice Items Loop

For the combined items table (time entries, extra goods, and expenses), use invoice.all_line_items in the PDF Designer:

{% for item in invoice.all_line_items %}
    {{ item.description }}              # Item description
    {{ item.quantity }}                 # Quantity
    {{ item.unit_price }}               # Unit price
    {{ item.total_amount }}             # Line total
{% endfor %}

For invoice items only (time-based billing):

{% for item in invoice.items %}
    {{ item.description }}              # Item description
    {{ item.quantity }}                 # Quantity (hours or units)
    {{ item.unit_price }}               # Unit price
    {{ item.total_amount }}             # Line total
    {{ item.time_entry_ids }}           # Associated time entry IDs
{% endfor %}

Extra Goods Loop

{% for good in invoice.extra_goods %}
    {{ good.name }}                     # Good/product name
    {{ good.description }}              # Description
    {{ good.sku }}                      # SKU code
    {{ good.category }}                 # Category
    {{ good.quantity }}                 # Quantity
    {{ good.unit_price }}               # Unit price
    {{ good.total_amount }}             # Line total
{% endfor %}

Settings Variables

{{ settings.company_name }}             # Your company name
{{ settings.company_address }}          # Your company address
{{ settings.company_email }}            # Your company email
{{ settings.company_phone }}            # Your company phone
{{ settings.company_website }}          # Your company website
{{ settings.company_tax_id }}           # Your tax ID
{{ settings.company_bank_info }}        # Bank information
{{ settings.currency }}                 # Currency code (e.g., "USD")
{{ settings.invoice_terms }}            # Default invoice terms

Helper Functions

{{ format_date(invoice.issue_date) }}           # Format date using Babel
{{ format_money(invoice.total_amount) }}        # Format money with currency
{{ get_logo_base64(logo_path) }}                # Get logo as base64 data URI
{{ _('Label') }}                                # Translate text (i18n)

Conditional Rendering

{% if settings.has_logo() %}
    <img src="{{ get_logo_base64(settings.get_logo_path()) }}" alt="Company Logo">
{% endif %}

{% if invoice.tax_rate > 0 %}
    <tr>
        <td>Tax ({{ invoice.tax_rate }}%):</td>
        <td>{{ format_money(invoice.tax_amount) }}</td>
    </tr>
{% endif %}

{% if invoice.notes %}
    <div class="notes">{{ invoice.notes }}</div>
{% endif %}

Example Templates

Basic Invoice Template

<div class="wrapper">
    <div class="invoice-header">
        <h1 class="company-name">{{ settings.company_name }}</h1>
        <div class="invoice-title">INVOICE</div>
    </div>
    
    <div class="meta">
        <p><strong>Invoice #:</strong> {{ invoice.invoice_number }}</p>
        <p><strong>Date:</strong> {{ format_date(invoice.issue_date) }}</p>
        <p><strong>Due:</strong> {{ format_date(invoice.due_date) }}</p>
    </div>
    
    <div class="client-info">
        <h3>Bill To:</h3>
        <p><strong>{{ invoice.client_name }}</strong></p>
        {% if invoice.client_email %}
        <p>{{ invoice.client_email }}</p>
        {% endif %}
    </div>
    
    <table>
        <thead>
            <tr>
                <th>Description</th>
                <th>Quantity</th>
                <th>Price</th>
                <th>Total</th>
            </tr>
        </thead>
        <tbody>
            {% for item in invoice.items %}
            <tr>
                <td>{{ item.description }}</td>
                <td>{{ "%.2f"|format(item.quantity) }}</td>
                <td>{{ format_money(item.unit_price) }}</td>
                <td>{{ format_money(item.total_amount) }}</td>
            </tr>
            {% endfor %}
        </tbody>
        <tfoot>
            <tr>
                <td colspan="3">Subtotal:</td>
                <td>{{ format_money(invoice.subtotal) }}</td>
            </tr>
            {% if invoice.tax_rate > 0 %}
            <tr>
                <td colspan="3">Tax ({{ invoice.tax_rate }}%):</td>
                <td>{{ format_money(invoice.tax_amount) }}</td>
            </tr>
            {% endif %}
            <tr>
                <td colspan="3"><strong>Total:</strong></td>
                <td><strong>{{ format_money(invoice.total_amount) }}</strong></td>
            </tr>
        </tfoot>
    </table>
    
    <div class="footer">
        <p><strong>{{ _('Terms & Conditions:') }}</strong> {{ settings.invoice_terms }}</p>
    </div>
</div>

Basic CSS Template

@page {
    size: A4;
    margin: 2cm;
}

body {
    font-family: Arial, sans-serif;
    font-size: 12pt;
    color: #333;
}

.wrapper {
    padding: 20px;
}

.invoice-header {
    display: flex;
    justify-content: space-between;
    border-bottom: 2px solid #007bff;
    padding-bottom: 15px;
    margin-bottom: 20px;
}

.company-name {
    font-size: 24pt;
    color: #007bff;
    margin: 0;
}

.invoice-title {
    font-size: 28pt;
    font-weight: bold;
    color: #007bff;
}

table {
    width: 100%;
    border-collapse: collapse;
    margin: 20px 0;
}

th, td {
    border: 1px solid #ddd;
    padding: 10px;
    text-align: left;
}

th {
    background-color: #f8f9fa;
    font-weight: bold;
}

tfoot td {
    font-weight: bold;
}

.footer {
    margin-top: 30px;
    padding-top: 15px;
    border-top: 1px solid #ddd;
}

Best Practices

1. Test Your Templates

Always preview your templates with real invoice data before saving:

  • Create a test invoice with various items
  • Use the preview function to check rendering
  • Test with and without optional fields (logo, notes, etc.)

2. Keep It Simple

  • Start with the default template and modify incrementally
  • Avoid overly complex layouts that may not render properly in PDF
  • Test with different amounts of data (few items vs. many items)

3. Use CSS for Styling

  • Keep HTML semantic and clean
  • Apply all styling through CSS
  • Use CSS variables for easy color/font customization

4. Handle Missing Data Gracefully

{% if invoice.client_email %}
    <p>Email: {{ invoice.client_email }}</p>
{% endif %}

5. Maintain Consistent Branding

  • Use company colors from your settings
  • Include your logo using the get_logo_base64() helper
  • Match font styles to your company branding

6. Consider Print Layout

@page {
    size: A4;
    margin: 2cm;
    @bottom-center {
        content: "Page " counter(page) " of " counter(pages);
    }
}

/* Avoid page breaks inside elements */
tr, td, th {
    page-break-inside: avoid;
}

Troubleshooting

Template Not Rendering

Issue: Template shows blank or errors in preview

Solutions:

  • Check Jinja2 syntax for typos
  • Ensure all {% %} blocks are properly closed
  • Verify variable names match documentation
  • Check browser console for JavaScript errors

Variables Not Displaying

Issue: Variables show as {{ variable_name }} instead of actual values

Solutions:

  • Ensure you're using correct variable names
  • Check if the data exists (use {% if variable %} checks)
  • Verify the variable is in scope for the template

CSS Not Applied

Issue: Styling doesn't appear in preview or PDF

Solutions:

  • Verify CSS syntax is valid
  • Check for CSS selector specificity issues
  • Ensure CSS is saved in the CSS field, not HTML
  • Test CSS separately in preview

Logo Not Displaying

Issue: Company logo doesn't appear in PDF

Solutions:

  • Verify logo is uploaded in Settings
  • Use get_logo_base64() helper function for reliable embedding
  • Check logo file format (PNG, JPG, GIF supported)
  • Ensure logo file size is reasonable (< 2MB)

Rate Limiting Errors

Issue: Preview or save fails with "Too Many Requests"

Solution:

  • Wait a minute before trying again
  • Rate limits: 60 previews/minute, 30 saves/minute

Items or Expenses Table Disappears After Save

Issue: After adding an Items Table or Expenses Table from the Invoice Data section and clicking Save, the tables disappeared from the design and were not present in the generated PDF.

Fix: The editor now persists table group names (items-table, expenses-table) in the saved design JSON and restores them when loading the layout. Tables should remain in the design after save and appear correctly in preview and export.

If you still see missing tables:

  • Ensure you add Items Table or Expenses Table from the left sidebar (Invoice Data section)
  • Use Reset to restore the default layout, then re-add the tables and save again
  • The Items Table uses invoice.all_line_items (time-based items, extra goods, and expenses in one table); see Invoice Extra Goods PDF Export for data sources

API Endpoints

GET /admin/pdf-layout

Display the PDF layout editor interface.

Permissions: Admin or manage_settings

POST /admin/pdf-layout

Save custom PDF template.

Parameters:

  • invoice_pdf_template_html: Custom HTML template
  • invoice_pdf_template_css: Custom CSS styles

Permissions: Admin or manage_settings

GET /admin/pdf-layout/default

Get default HTML and CSS templates.

Response: JSON with html and css keys

Permissions: Admin or manage_settings

POST /admin/pdf-layout/preview

Generate preview of custom template.

Parameters:

  • html: HTML template to preview
  • css: CSS styles to apply
  • invoice_id (optional): Specific invoice to preview

Response: Rendered HTML preview

Permissions: Admin or manage_settings

POST /admin/pdf-layout/reset

Reset templates to defaults (clear custom templates).

Permissions: Admin or manage_settings

Technical Details

Template Rendering

  1. Priority: Custom templates take precedence over defaults
  2. Engine: Jinja2 template engine with Flask context
  3. PDF Generation: WeasyPrint (fallback to ReportLab if unavailable)
  4. Storage: Templates stored in Settings table in database

Security Considerations

  • All templates are sanitized before rendering
  • CSRF protection on all POST endpoints
  • Rate limiting prevents abuse
  • Only admin users can modify templates
  • Templates are executed server-side in controlled environment

Performance

  • Templates are cached per invoice generation
  • Preview uses same rendering engine as PDF generation
  • Large templates may take longer to render
  • Optimize images and avoid external resources

Internationalization (i18n)

Use the _() function to translate text:

<th>{{ _('Description') }}</th>
<th>{{ _('Quantity') }}</th>
<th>{{ _('Price') }}</th>

Supported languages:

  • English (en)
  • German (de)
  • French (fr)
  • Italian (it)
  • Dutch (nl)
  • Finnish (fi)

Advanced Features

Page Numbers

@page {
    @bottom-center {
        content: "Page " counter(page) " of " counter(pages);
        font-size: 10pt;
    }
}

Conditional Styling

<div class="status-{{ invoice.status }}">
    Status: {{ invoice.status|title }}
</div>
.status-paid { color: green; }
.status-overdue { color: red; }
.status-draft { color: gray; }

Custom Filters

{{ invoice.client_name|upper }}
{{ invoice.total_amount|round(2) }}
{{ invoice.issue_date|string }}

Migration from Old Templates

If you have existing invoice templates:

  1. Backup: Export your current template code
  2. Test: Create test invoices to validate
  3. Convert: Adapt any custom logic to new format
  4. Preview: Use preview function extensively
  5. Deploy: Save and test with real invoices
  6. Monitor: Check generated PDFs for issues

Support and Resources

Konva.js Visual Editor Features

Keyboard Shortcuts

The visual editor supports these keyboard shortcuts:

  • Delete/Backspace: Remove selected element
  • Ctrl+C: Copy selected element
  • Ctrl+V: Paste copied element (offset by 20px)
  • Ctrl+D: Duplicate selected element
  • Arrow Keys: Move element by 1px
  • Shift+Arrow Keys: Move element by 10px

Element Types

Text Elements

All text elements support:

  • Custom text content (with Jinja2 variables)
  • Font family (6 fonts available)
  • Font size (pixels)
  • Font style (normal, bold, italic)
  • Text color (color picker)
  • Width (for text wrapping)
  • Opacity (0-100%)

Shape Elements

Rectangles and circles support:

  • Fill color (interior)
  • Stroke color (border)
  • Stroke width (border thickness)
  • Dimensions (width/height for rectangles, radius for circles)
  • Opacity

Special Elements

  • Logo: Displays uploaded company logo (if available)
  • Items Table: Dynamic table with headers and item rows
  • QR Code: Placeholder for QR code generation
  • Barcode: Placeholder for barcode generation
  • Watermark: Large, semi-transparent text overlay

Alignment Tools

Use toolbar buttons to align selected elements:

  • Align Left: Move to left edge
  • Center Horizontally: Center on canvas
  • Align Right: Move to right edge
  • Align Top: Move to top edge
  • Center Vertically: Center vertically
  • Align Bottom: Move to bottom edge

Layer Management

Control element stacking order:

  • Move Up: Bring forward one layer
  • Move Down: Send back one layer
  • Bring to Top: Bring to front
  • Send to Bottom: Send to back

Changelog

Version 2.0 (Current - Enhanced with Konva.js)

  • New: Konva.js-powered visual editor
  • New: Drag-and-drop element library with 30+ elements
  • New: Real-time properties panel
  • New: Shape elements (rectangles, circles)
  • New: Alignment tools
  • New: Layer management (z-index controls)
  • New: Keyboard shortcuts
  • New: Copy/paste/duplicate functionality
  • New: Transform handles for resizing
  • New: Live canvas editing with instant visual feedback
  • Improved: Enhanced preview integration
  • Improved: Better element positioning and sizing

Version 1.0

  • Initial PDF layout customization system
  • GrapesJS visual editor (deprecated)
  • Real-time preview
  • System-wide template storage
  • Jinja2 template variables
  • Rate limiting and security features