Files
TimeTracker/app/models/invoice_pdf_template.py
T
Dries Peeters a94e928509 feat: Add support for multiple PDF template page sizes
Add the ability to create and manage PDF invoice templates for different
page sizes (A4, Letter, Legal, A3, A5, Tabloid) with independent templates
for each size.

Features:
- Database migration to create invoice_pdf_templates table with page_size
  column and default templates for all supported sizes
- New InvoicePDFTemplate model with helper methods for template management
- Page size selector dropdown in canvas editor with dynamic canvas resizing
- Size selection in invoice export view
- Each page size maintains its own template (HTML, CSS, design JSON)
- Preview functionality converted to full-screen modal popup

PDF Generation:
- Updated InvoicePDFGenerator to accept page_size parameter
- Dynamic @page rule updates in CSS based on selected size
- Removed conflicting @page rules from HTML inline styles when separate
  CSS exists
- Template content preserved exactly as saved (no whitespace stripping)
- Fallback logic: size-specific template → legacy Settings template → default

UI/UX Improvements:
- Styled page size selector to match app theme with dark mode support
- Fixed canvas editor header styling and readability
- Canvas correctly resizes when switching between page sizes
- Unsaved changes confirmation uses app's standard modal
- All editor controls properly styled for dark/light mode
- Preview opens in modal instead of small side window

Bug Fixes:
- Fixed migration KeyError by correcting down_revision reference
- Fixed DatatypeMismatch error by using boolean TRUE instead of integer
- Fixed template content mismatch (logo positions) by preserving HTML
- Fixed page size not being applied by ensuring @page rules are updated
- Fixed f-string syntax error in _generate_css by using .format() instead
- Fixed debug_print scope issue in _render_from_custom_template

Debugging:
- Added comprehensive debug logging to PDF generation flow
- Debug output visible in Docker console for troubleshooting
- Logs template retrieval, @page size updates, and final CSS content

Files Changed:
- migrations/versions/041_add_invoice_pdf_templates_table.py (new)
- app/models/invoice_pdf_template.py (new)
- app/models/__init__.py (register new model)
- app/routes/admin.py (template management by size)
- app/routes/invoices.py (page size parameter, debug logging)
- app/utils/pdf_generator.py (page size support, debug logging)
- templates/admin/pdf_layout.html (size selector, canvas resizing, modal)
- app/templates/invoices/view.html (size selector for export)
2025-11-03 11:48:41 +01:00

115 lines
4.2 KiB
Python

"""Invoice PDF Template Model
Stores PDF templates for different page sizes (A4, Letter, A3, etc.)
"""
from datetime import datetime
from app import db
class InvoicePDFTemplate(db.Model):
"""Model for storing invoice PDF templates by page size"""
__tablename__ = 'invoice_pdf_templates'
id = db.Column(db.Integer, primary_key=True)
page_size = db.Column(db.String(20), nullable=False, unique=True) # A4, Letter, A3, Legal, A5, etc.
template_html = db.Column(db.Text, nullable=True)
template_css = db.Column(db.Text, nullable=True)
design_json = db.Column(db.Text, nullable=True) # Konva.js design state
is_default = db.Column(db.Boolean, default=False, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Standard page sizes and their dimensions in mm (for reference)
PAGE_SIZES = {
'A4': {'width': 210, 'height': 297},
'Letter': {'width': 216, 'height': 279},
'Legal': {'width': 216, 'height': 356},
'A3': {'width': 297, 'height': 420},
'A5': {'width': 148, 'height': 210},
'Tabloid': {'width': 279, 'height': 432},
}
def __repr__(self):
return f'<InvoicePDFTemplate {self.page_size}>'
@classmethod
def get_template(cls, page_size='A4'):
"""Get template for a specific page size, create default if doesn't exist"""
template = cls.query.filter_by(page_size=page_size).first()
if not template:
# Create default template for this size
template = cls(
page_size=page_size,
template_html='',
template_css='',
design_json='',
is_default=True
)
db.session.add(template)
try:
db.session.commit()
except Exception:
db.session.rollback()
# Try to get again in case it was created concurrently
template = cls.query.filter_by(page_size=page_size).first()
if not template:
raise
return template
@classmethod
def get_all_templates(cls):
"""Get all templates ordered by page size"""
return cls.query.order_by(cls.page_size).all()
@classmethod
def get_default_template(cls):
"""Get the default template (A4)"""
return cls.get_template('A4')
@classmethod
def ensure_default_templates(cls):
"""Ensure all default templates exist"""
default_sizes = ['A4', 'Letter', 'Legal', 'A3', 'A5']
for size in default_sizes:
template = cls.query.filter_by(page_size=size).first()
if not template:
template = cls(
page_size=size,
template_html='',
template_css='',
design_json='',
is_default=True
)
db.session.add(template)
try:
db.session.commit()
except Exception:
db.session.rollback()
def to_dict(self):
"""Convert template to dictionary"""
return {
'id': self.id,
'page_size': self.page_size,
'template_html': self.template_html or '',
'template_css': self.template_css or '',
'design_json': self.design_json or '',
'is_default': self.is_default,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}
def get_page_dimensions_mm(self):
"""Get page dimensions in mm"""
return self.PAGE_SIZES.get(self.page_size, {'width': 210, 'height': 297})
def get_page_dimensions_px(self, dpi=72):
"""Get page dimensions in pixels at given DPI"""
dims_mm = self.get_page_dimensions_mm()
# Convert mm to pixels: 1 mm = (dpi / 25.4) pixels
width_px = int((dims_mm['width'] / 25.4) * dpi)
height_px = int((dims_mm['height'] / 25.4) * dpi)
return {'width': width_px, 'height': height_px}