Files
TimeTracker/docs/PDF_LAYOUT_CUSTOMIZATION.md
Dries Peeters 20b7401891 feat: Add invoice expenses, enhanced PDF editor with Konva.js, and uploads persistence
Major Features:
- Invoice Expenses: Allow linking billable expenses to invoices with automatic total calculations
  - Add expenses to invoices via "Generate from Time/Costs" workflow
  - Display expenses in invoice view, edit forms, and PDF exports
  - Track expense states (approved, invoiced, reimbursed) with automatic unlinking on invoice deletion
  - Update PDF generator and CSV exports to include expense line items

- Enhanced PDF Invoice Editor: Complete redesign using Konva.js for visual drag-and-drop layout design
  - Add 40+ draggable elements (company info, invoice data, shapes, text, advanced elements)
  - Implement comprehensive properties panel for precise element customization (position, fonts, colors, opacity)
  - Add canvas toolbar with alignment tools, zoom controls, and layer management
  - Support keyboard shortcuts (copy/paste, duplicate, arrow key positioning)
  - Save designs as JSON for editing and generate clean HTML/CSS for rendering
  - Add real-time preview with live data

- Uploads Persistence: Implement Docker volume persistence for user-uploaded files
  - Add app_uploads volume to all Docker Compose configurations
  - Ensure company logos and avatars persist across container rebuilds and restarts
  - Create migration script for existing installations
  - Update directory structure with proper permissions (755 for dirs, 644 for files)

Database & Backend:
- Add invoice_pdf_design_json column to settings table via Alembic migration
- Extend Invoice model with expenses relationship
- Update admin routes for PDF layout designer endpoints
- Enhance invoice routes to handle expense linking/unlinking

Frontend & UI:
- Redesign PDF layout editor template with Konva.js canvas (2484 lines, major overhaul)
- Update invoice edit/view templates to display and manage expenses
- Add expense sections to invoice forms with unlink functionality
- Enhance UI components with keyboard shortcuts support
- Update multiple templates for consistency and accessibility

Testing & Documentation:
- Add comprehensive test suites for invoice expenses, PDF layouts, and uploads persistence
- Create detailed documentation for all new features (5 new docs)
- Include migration guides and troubleshooting sections

Infrastructure:
- Update docker-compose files (main, example, remote, remote-dev, local-test) with uploads volume
- Configure pytest for new test modules
- Add template filters for currency formatting and expense display

This update significantly enhances TimeTracker's invoice management capabilities,
improves the PDF customization experience, and ensures uploaded files persist
reliably across deployments.
2025-10-29 15:03:01 +01:00

16 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. Navigate to AdminPDF Layout in the sidebar
  3. The PDF Layout Editor page will open

URL: /admin/pdf-layout

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)
    • 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 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

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