mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-01 01:40:59 -05:00
a94e928509
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)
115 lines
4.2 KiB
Python
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}
|
|
|