mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-24 07:10:21 -05:00
35e3694cc1
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.
643 lines
18 KiB
Markdown
643 lines
18 KiB
Markdown
# 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](./PDF_EDITOR_QUICK_START.md)
|
|
|
|
For comprehensive feature documentation, see [Enhanced PDF Editor Features](./PDF_EDITOR_ENHANCED_FEATURES.md)
|
|
|
|
## Available Template Variables
|
|
|
|
### Invoice Variables
|
|
|
|
```jinja
|
|
{{ 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
|
|
|
|
```jinja
|
|
{{ 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:
|
|
|
|
```jinja
|
|
{% 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):
|
|
|
|
```jinja
|
|
{% 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
|
|
|
|
```jinja
|
|
{% 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
|
|
|
|
```jinja
|
|
{{ 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
|
|
|
|
```jinja
|
|
{{ 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
|
|
|
|
```jinja
|
|
{% 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
|
|
|
|
```html
|
|
<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
|
|
|
|
```css
|
|
@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
|
|
|
|
```jinja
|
|
{% 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
|
|
|
|
```css
|
|
@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](INVOICE_EXTRA_GOODS_PDF_EXPORT.md) 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:
|
|
|
|
```jinja
|
|
<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
|
|
|
|
```css
|
|
@page {
|
|
@bottom-center {
|
|
content: "Page " counter(page) " of " counter(pages);
|
|
font-size: 10pt;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Conditional Styling
|
|
|
|
```jinja
|
|
<div class="status-{{ invoice.status }}">
|
|
Status: {{ invoice.status|title }}
|
|
</div>
|
|
```
|
|
|
|
```css
|
|
.status-paid { color: green; }
|
|
.status-overdue { color: red; }
|
|
.status-draft { color: gray; }
|
|
```
|
|
|
|
### Custom Filters
|
|
|
|
```jinja
|
|
{{ 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
|
|
|
|
- **Default Template**: View source at `app/templates/invoices/pdf_default.html`
|
|
- **Default CSS**: View source at `templates/invoices/pdf_styles_default.css`
|
|
- **Jinja2 Documentation**: https://jinja.palletsprojects.com/
|
|
- **WeasyPrint Documentation**: https://weasyprint.org/
|
|
- **CSS Print Styles**: https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/
|
|
|
|
## 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
|
|
|