diff --git a/app/models/invoice_email.py b/app/models/invoice_email.py index 96c5530..53a20af 100644 --- a/app/models/invoice_email.py +++ b/app/models/invoice_email.py @@ -1,5 +1,11 @@ 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): @@ -13,7 +19,7 @@ class InvoiceEmail(db.Model): # 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=datetime.utcnow) + sent_at = db.Column(db.DateTime, nullable=False, default=local_now) sent_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) # Tracking @@ -31,8 +37,8 @@ class InvoiceEmail(db.Model): error_message = db.Column(db.Text, nullable=True) # Error message if send failed # Metadata - 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) + 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') @@ -52,8 +58,8 @@ class InvoiceEmail(db.Model): def mark_opened(self): """Mark email as opened""" if not self.opened_at: - self.opened_at = datetime.utcnow() - self.last_opened_at = datetime.utcnow() + self.opened_at = local_now() + self.last_opened_at = local_now() self.opened_count += 1 if self.status == 'sent': self.status = 'opened' @@ -61,7 +67,7 @@ class InvoiceEmail(db.Model): def mark_paid(self): """Mark invoice as paid (after email was sent)""" if not self.paid_at: - self.paid_at = datetime.utcnow() + self.paid_at = local_now() self.status = 'paid' def mark_failed(self, error_message): diff --git a/app/routes/invoices.py b/app/routes/invoices.py index aeb0871..882dd63 100644 --- a/app/routes/invoices.py +++ b/app/routes/invoices.py @@ -214,7 +214,13 @@ def view_invoice(invoice_id): # Get email templates for selection email_templates = InvoiceTemplate.query.order_by(InvoiceTemplate.name).all() - return render_template('invoices/view.html', invoice=invoice, email_templates=email_templates) + # Get email history + from app.models import InvoiceEmail + email_history = InvoiceEmail.query.filter_by(invoice_id=invoice_id)\ + .order_by(InvoiceEmail.sent_at.desc())\ + .all() + + return render_template('invoices/view.html', invoice=invoice, email_templates=email_templates, email_history=email_history) @invoices_bp.route('/invoices//edit', methods=['GET', 'POST']) @login_required @@ -989,3 +995,85 @@ def send_invoice_email_route(invoice_id): logger.error(f"Error sending invoice email: {type(e).__name__}: {str(e)}") logger.exception("Full error traceback:") return jsonify({'error': f'Failed to send email: {str(e)}'}), 500 + + +@invoices_bp.route('/invoices//email-history', methods=['GET']) +@login_required +def get_invoice_email_history(invoice_id): + """Get email history for an invoice""" + invoice = Invoice.query.get_or_404(invoice_id) + + # Check access permissions + if not current_user.is_admin and invoice.created_by != current_user.id: + return jsonify({'error': 'Permission denied'}), 403 + + from app.models import InvoiceEmail + + # Get all email records for this invoice, ordered by most recent first + email_records = InvoiceEmail.query.filter_by(invoice_id=invoice_id)\ + .order_by(InvoiceEmail.sent_at.desc())\ + .all() + + # Convert to list of dictionaries + email_history = [email.to_dict() for email in email_records] + + return jsonify({ + 'success': True, + 'email_history': email_history, + 'count': len(email_history) + }) + + +@invoices_bp.route('/invoices//resend-email/', methods=['POST']) +@login_required +def resend_invoice_email(invoice_id, email_id): + """Resend an invoice email""" + invoice = Invoice.query.get_or_404(invoice_id) + + # Check access permissions + if not current_user.is_admin and invoice.created_by != current_user.id: + return jsonify({'error': 'Permission denied'}), 403 + + from app.models import InvoiceEmail + original_email = InvoiceEmail.query.get_or_404(email_id) + + # Verify the email belongs to this invoice + if original_email.invoice_id != invoice_id: + return jsonify({'error': 'Email record does not belong to this invoice'}), 400 + + # Get recipient email from request or use original + recipient_email = request.form.get('recipient_email', '').strip() or request.json.get('recipient_email', '').strip() if request.is_json else '' + if not recipient_email: + recipient_email = original_email.recipient_email + + # Get custom message if provided + custom_message = request.form.get('custom_message', '').strip() or (request.json.get('custom_message', '').strip() if request.is_json else '') + + # Get email template ID if provided + email_template_id = request.form.get('email_template_id', type=int) or (request.json.get('email_template_id') if request.is_json else None) + + try: + from app.utils.email import send_invoice_email + + success, invoice_email, message = send_invoice_email( + invoice=invoice, + recipient_email=recipient_email, + sender_user=current_user, + custom_message=custom_message if custom_message else None, + email_template_id=email_template_id + ) + + if success: + flash(f'Invoice email resent successfully to {recipient_email}', 'success') + return jsonify({ + 'success': True, + 'message': message, + 'invoice_email_id': invoice_email.id if invoice_email else None + }) + else: + return jsonify({'error': message}), 500 + + except Exception as e: + logger.error(f"Error resending invoice email: {type(e).__name__}: {str(e)}") + logger.exception("Full error traceback:") + return jsonify({'error': f'Failed to resend email: {str(e)}'}), 500 diff --git a/app/templates/admin/email_support.html b/app/templates/admin/email_support.html index 82ac4d7..b57b137 100644 --- a/app/templates/admin/email_support.html +++ b/app/templates/admin/email_support.html @@ -20,7 +20,7 @@

- {{ _('Configure email settings here to save them in the database. Database settings take precedence over environment variables.') }} + {{ _('Configure email settings here to save them in the database.') }}

@@ -119,7 +119,7 @@
{{ _('Email is not configured') }} - {{ _('Please configure email settings in your environment variables.') }} + {{ _('Please configure email settings using the form above.') }}
@@ -228,23 +228,6 @@

{{ _('Configuration Guide') }}

-

{{ _('To configure email, set the following environment variables:') }}

- -
-
# {{ _('Basic SMTP Settings') }}
-
MAIL_SERVER=smtp.gmail.com
-
MAIL_PORT=587
-
MAIL_USE_TLS=true
-
MAIL_USE_SSL=false
-

-
# {{ _('Authentication') }}
-
MAIL_USERNAME=your-email@gmail.com
-
MAIL_PASSWORD=your-app-password
-

-
# {{ _('Sender Information') }}
-
MAIL_DEFAULT_SENDER=noreply@yourdomain.com
-
-

{{ _('Common SMTP Providers') }}