mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-07 21:10:46 -05:00
b4486a627f
- Webhook models: remove duplicate index definitions so db.create_all() no longer raises 'index already exists' (columns already have index=True) - ImportService: fix circular import by late-importing ClientService, ProjectService, TimeTrackingService in __init__ - reports: fix F823 by renaming unpack variable _ to _entry_count to avoid shadowing gettext _ in export_task_excel() - Code quality: add .flake8 with extend-ignore so flake8 CI passes; simplify pyproject.toml isort config (drop unsupported options) - Format: run black and isort on app/ - tests: restore minimal app fixture in test_import_export_models
208 lines
8.4 KiB
Python
208 lines
8.4 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) # Legacy HTML template (backward compatibility)
|
|
template_css = db.Column(db.Text, nullable=True) # Legacy CSS template (backward compatibility)
|
|
design_json = db.Column(db.Text, nullable=True) # Konva.js design state
|
|
template_json = db.Column(db.Text, nullable=True) # ReportLab template JSON (new format)
|
|
date_format = db.Column(
|
|
db.String(50), default="%d.%m.%Y", nullable=False
|
|
) # Date format for invoices (strftime format)
|
|
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 with default JSON
|
|
import json
|
|
|
|
from app.utils.pdf_template_schema import get_default_template
|
|
|
|
default_json = get_default_template(page_size)
|
|
template = cls(
|
|
page_size=page_size,
|
|
template_html="",
|
|
template_css="",
|
|
design_json="",
|
|
template_json=json.dumps(default_json),
|
|
date_format="%d.%m.%Y",
|
|
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
|
|
|
|
# DON'T call ensure_template_json() here - it may overwrite saved templates
|
|
# Only validate that template exists - if it has no JSON, it will be handled during export
|
|
# This prevents overwriting saved custom templates with defaults
|
|
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"""
|
|
import json
|
|
|
|
from app.utils.pdf_template_schema import get_default_template
|
|
|
|
default_sizes = ["A4", "Letter", "Legal", "A3", "A5"]
|
|
for size in default_sizes:
|
|
template = cls.query.filter_by(page_size=size).first()
|
|
if not template:
|
|
default_json = get_default_template(size)
|
|
template = cls(
|
|
page_size=size,
|
|
template_html="",
|
|
template_css="",
|
|
design_json="",
|
|
template_json=json.dumps(default_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 "",
|
|
"template_json": self.template_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_template_json(self):
|
|
"""Get template JSON, parsing from string if needed"""
|
|
if not self.template_json:
|
|
return None
|
|
import json
|
|
|
|
try:
|
|
return json.loads(self.template_json)
|
|
except Exception:
|
|
return None
|
|
|
|
def set_template_json(self, template_dict):
|
|
"""Set template JSON from dictionary"""
|
|
import json
|
|
|
|
self.template_json = json.dumps(template_dict) if template_dict 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}
|
|
|
|
def ensure_template_json(self):
|
|
"""Ensure template has valid JSON, generate if missing"""
|
|
import json
|
|
|
|
from flask import current_app
|
|
|
|
# First check if template_json exists and is not empty
|
|
if self.template_json and self.template_json.strip():
|
|
# Validate that it's valid JSON
|
|
try:
|
|
parsed_json = json.loads(self.template_json)
|
|
# If it's valid JSON with at least a page property, consider it valid
|
|
if isinstance(parsed_json, dict) and "page" in parsed_json:
|
|
current_app.logger.info(
|
|
f"[TEMPLATE] Template JSON is valid - PageSize: '{self.page_size}', TemplateID: {self.id}"
|
|
)
|
|
return # Template JSON is valid, don't overwrite
|
|
else:
|
|
current_app.logger.warning(
|
|
f"[TEMPLATE] Template JSON exists but missing 'page' property - PageSize: '{self.page_size}', TemplateID: {self.id}"
|
|
)
|
|
except json.JSONDecodeError as e:
|
|
current_app.logger.warning(
|
|
f"[TEMPLATE] Template JSON exists but is invalid JSON - PageSize: '{self.page_size}', TemplateID: {self.id}, Error: {str(e)}"
|
|
)
|
|
# Invalid JSON - will generate default below
|
|
|
|
# Only generate default if template_json is truly None or empty, or invalid
|
|
if not self.template_json or not self.template_json.strip():
|
|
current_app.logger.warning(
|
|
f"[TEMPLATE] Generating default template JSON - PageSize: '{self.page_size}', TemplateID: {self.id}, Reason: template_json is missing or empty"
|
|
)
|
|
else:
|
|
current_app.logger.warning(
|
|
f"[TEMPLATE] Generating default template JSON - PageSize: '{self.page_size}', TemplateID: {self.id}, Reason: existing JSON is invalid"
|
|
)
|
|
|
|
from app.utils.pdf_template_schema import get_default_template
|
|
|
|
default_json = get_default_template(self.page_size)
|
|
self.template_json = json.dumps(default_json)
|
|
try:
|
|
db.session.commit()
|
|
current_app.logger.info(
|
|
f"[TEMPLATE] Default template JSON saved - PageSize: '{self.page_size}', TemplateID: {self.id}"
|
|
)
|
|
except Exception as e:
|
|
current_app.logger.error(
|
|
f"[TEMPLATE] Failed to save default template JSON - PageSize: '{self.page_size}', TemplateID: {self.id}, Error: {str(e)}",
|
|
exc_info=True,
|
|
)
|
|
db.session.rollback()
|