Files
TimeTracker/app/models/invoice_email.py
Dries Peeters a4797b25ac fix: Fix email template editor initialization and JavaScript errors
- Fix script block name from extra_js to scripts_extra to match base.html
- Replace inline onclick handlers with event listeners to fix scope issues
- Fix ReferenceError for toggleViewMode and insertVariable functions
- Improve editor initialization flow with proper script loading detection
- Add error handling and fallback to textarea if Toast UI Editor fails to load
- Add debug logging for troubleshooting initialization issues
- Ensure default templates are editable (no restrictions in backend)
- Add email templates link to admin menu in base.html
- Remove ENV file configuration details from email support page

The editor now properly initializes and all interactive features work correctly.
2025-11-14 13:40:00 +01:00

101 lines
3.9 KiB
Python

from datetime import datetime
from app import db
from app.utils.timezone import now_in_app_timezone
def local_now():
"""Get current time in local timezone as naive datetime (for database storage)"""
return now_in_app_timezone().replace(tzinfo=None)
class InvoiceEmail(db.Model):
"""Model for tracking invoice emails sent to clients"""
__tablename__ = 'invoice_emails'
id = db.Column(db.Integer, primary_key=True)
invoice_id = db.Column(db.Integer, db.ForeignKey('invoices.id'), nullable=False, index=True)
# Email details
recipient_email = db.Column(db.String(200), nullable=False)
subject = db.Column(db.String(500), nullable=False)
sent_at = db.Column(db.DateTime, nullable=False, default=local_now)
sent_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
# Tracking
opened_at = db.Column(db.DateTime, nullable=True) # When email was opened (if tracked)
opened_count = db.Column(db.Integer, nullable=False, default=0) # Number of times opened
last_opened_at = db.Column(db.DateTime, nullable=True) # Last time email was opened
# Payment tracking
paid_at = db.Column(db.DateTime, nullable=True) # When invoice was marked as paid (if after email)
# Status
status = db.Column(db.String(20), nullable=False, default='sent') # 'sent', 'opened', 'paid', 'bounced', 'failed'
# Error tracking
error_message = db.Column(db.Text, nullable=True) # Error message if send failed
# Metadata
created_at = db.Column(db.DateTime, default=local_now, nullable=False)
updated_at = db.Column(db.DateTime, default=local_now, onupdate=local_now, nullable=False)
# Relationships
invoice = db.relationship('Invoice', backref='email_records')
sender = db.relationship('User', backref='sent_invoice_emails')
def __init__(self, invoice_id, recipient_email, subject, sent_by, **kwargs):
self.invoice_id = invoice_id
self.recipient_email = recipient_email
self.subject = subject
self.sent_by = sent_by
self.status = kwargs.get('status', 'sent')
self.error_message = kwargs.get('error_message')
def __repr__(self):
return f'<InvoiceEmail {self.invoice_id} -> {self.recipient_email} ({self.status})>'
def mark_opened(self):
"""Mark email as opened"""
if not self.opened_at:
self.opened_at = local_now()
self.last_opened_at = local_now()
self.opened_count += 1
if self.status == 'sent':
self.status = 'opened'
def mark_paid(self):
"""Mark invoice as paid (after email was sent)"""
if not self.paid_at:
self.paid_at = local_now()
self.status = 'paid'
def mark_failed(self, error_message):
"""Mark email send as failed"""
self.status = 'failed'
self.error_message = error_message
def mark_bounced(self):
"""Mark email as bounced"""
self.status = 'bounced'
def to_dict(self):
"""Convert invoice email to dictionary"""
return {
'id': self.id,
'invoice_id': self.invoice_id,
'recipient_email': self.recipient_email,
'subject': self.subject,
'sent_at': self.sent_at.isoformat() if self.sent_at else None,
'sent_by': self.sent_by,
'opened_at': self.opened_at.isoformat() if self.opened_at else None,
'opened_count': self.opened_count,
'last_opened_at': self.last_opened_at.isoformat() if self.last_opened_at else None,
'paid_at': self.paid_at.isoformat() if self.paid_at else None,
'status': self.status,
'error_message': self.error_message,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
}