Files
MarineMaintenance/mailer.py
T
alro65 67a0e674ca Initial commit — MarineMaintenance v1.0
Marine maintenance management: work orders with photos, ISM/SWP procedures,
MSDS, inventory, RFQ/purchases, vessel history, bilingual PDF reports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 01:54:20 -04:00

211 lines
9.2 KiB
Python

"""
mailer.py — Envío de email con PDF adjunto via SMTP
"""
import smtplib
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
import sqlite3, os
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'marine_maintenance.db')
def get_email_config(company_id):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cfg = None
if company_id:
cfg = conn.execute("SELECT * FROM email_config WHERE company_id=?", (company_id,)).fetchone()
if not cfg:
cfg = conn.execute("SELECT * FROM email_config WHERE company_id IS NULL").fetchone()
if not cfg:
cfg = conn.execute("SELECT * FROM email_config LIMIT 1").fetchone()
conn.close()
return dict(cfg) if cfg else None
def _smtp_send(host, port, user, pwd, use_tls, from_addr, to_addrs, msg_str):
"""Envío SMTP puro — abre conexión fresca cada vez."""
if port == 465:
with smtplib.SMTP_SSL(host, port, timeout=30) as s:
s.login(user, pwd)
s.sendmail(from_addr, to_addrs, msg_str)
else:
with smtplib.SMTP(host, port, timeout=30) as s:
s.ehlo()
if use_tls:
s.starttls()
s.ehlo()
s.login(user, pwd)
s.sendmail(from_addr, to_addrs, msg_str)
def send_report_email(to_email, to_name, subject, body_html,
pdf_bytes, pdf_filename, company_id):
cfg = get_email_config(company_id)
if not cfg or not cfg.get('smtp_host'):
return False, "Email no configurado. Ve a Configuración → Email."
host = cfg['smtp_host']
port = int(cfg.get('smtp_port', 587))
user = cfg['smtp_user']
pwd = cfg['smtp_password']
from_addr = cfg.get('from_email') or user
from_name = cfg.get('from_name', '')
use_tls = bool(cfg.get('use_tls', 1))
if not host or not user or not pwd:
return False, "Configuración SMTP incompleta."
# ── Email al cliente (con PDF adjunto) ────────────────────────────────────
msg = MIMEMultipart('mixed')
msg['From'] = f"{from_name} <{from_addr}>" if from_name else from_addr
msg['To'] = f"{to_name} <{to_email}>" if to_name else to_email
msg['Subject'] = subject
body_part = MIMEMultipart('alternative')
body_part.attach(MIMEText(body_html, 'html', 'utf-8'))
msg.attach(body_part)
if pdf_bytes:
pdf_part = MIMEApplication(pdf_bytes, _subtype='pdf')
pdf_part.add_header('Content-Disposition', 'attachment', filename=pdf_filename)
msg.attach(pdf_part)
try:
_smtp_send(host, port, user, pwd, use_tls, from_addr, [to_email], msg.as_string())
except smtplib.SMTPAuthenticationError as e:
return False, f"Error de autenticación: {e}"
except smtplib.SMTPConnectError as e:
return False, f"No se pudo conectar a {host}:{port}: {e}"
except smtplib.SMTPServerDisconnected as e:
return False, f"Servidor cerró la conexión: {e}"
except smtplib.SMTPException as e:
return False, f"Error SMTP: {e}"
except OSError as e:
return False, f"Error de red: {e}"
except Exception as e:
return False, f"{type(e).__name__}: {e}"
# ── Copia al remitente CON PDF adjunto ────────────────────────────────────
try:
msg_copy = MIMEMultipart('mixed')
msg_copy['From'] = f"{from_name} <{from_addr}>" if from_name else from_addr
msg_copy['To'] = from_addr
msg_copy['Subject'] = f"[COPIA] {subject}"
body_copy = MIMEMultipart('alternative')
body_copy.attach(MIMEText(body_html, 'html', 'utf-8'))
msg_copy.attach(body_copy)
if pdf_bytes:
pdf_copy = MIMEApplication(pdf_bytes, _subtype='pdf')
pdf_copy.add_header('Content-Disposition', 'attachment', filename=pdf_filename)
msg_copy.attach(pdf_copy)
_smtp_send(host, port, user, pwd, use_tls, from_addr, [from_addr], msg_copy.as_string())
except Exception:
pass # copia falla silenciosamente
return True, ''
def build_email_body(order, vessel, company_name, app_url=''):
order_num = order.get('order_number', '')
scope = order.get('scope') or order.get('description', '')[:100]
status = order.get('status', '').replace('_', ' ').title()
total = float(order.get('total_labor_cost') or 0) + float(order.get('total_parts_cost') or 0)
vessel_n = vessel.get('name', '')
date_end = order.get('end_date') or order.get('start_date') or ''
pdf_link = f"{app_url}/work-orders/{order.get('id')}/pdf" if app_url else ''
return f"""<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body style="font-family:Arial,sans-serif;background:#f4f7fb;margin:0;padding:20px">
<div style="max-width:600px;margin:0 auto;background:white;border-radius:10px;
overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,0.1)">
<div style="background:#0a1628;padding:24px 30px;text-align:center">
<div style="font-size:28px;margin-bottom:6px">⚓</div>
<div style="color:#00b4d8;font-size:20px;font-weight:700;letter-spacing:2px">{company_name.upper()}</div>
<div style="color:#8a9bb0;font-size:12px;letter-spacing:2px">REPORTE DE MANTENIMIENTO</div>
</div>
<div style="padding:28px 30px">
<div style="font-size:13px;color:#8a9bb0;margin-bottom:4px">Orden de Trabajo</div>
<div style="font-size:24px;font-weight:700;color:#0a1628;margin-bottom:16px">{order_num}</div>
<table style="width:100%;border-collapse:collapse;font-size:13px;margin-bottom:20px">
<tr style="background:#f0f4f8">
<td style="padding:10px 14px;color:#8a9bb0;width:40%"><b>Embarcación</b></td>
<td style="padding:10px 14px;color:#0a1628">{vessel_n}</td>
</tr>
<tr>
<td style="padding:10px 14px;color:#8a9bb0"><b>Trabajo</b></td>
<td style="padding:10px 14px;color:#0a1628">{scope}</td>
</tr>
<tr style="background:#f0f4f8">
<td style="padding:10px 14px;color:#8a9bb0"><b>Fecha</b></td>
<td style="padding:10px 14px;color:#0a1628">{date_end}</td>
</tr>
<tr>
<td style="padding:10px 14px;color:#8a9bb0"><b>Estado</b></td>
<td style="padding:10px 14px">
<span style="background:#e0f7fa;color:#00838f;padding:2px 10px;
border-radius:12px;font-size:11px;font-weight:700">{status}</span>
</td>
</tr>
<tr style="background:#f0f4f8">
<td style="padding:10px 14px;color:#8a9bb0"><b>Total</b></td>
<td style="padding:10px 14px;color:#0a1628;font-weight:700;font-size:16px">${total:.2f}</td>
</tr>
</table>
<p style="font-size:13px;color:#555;line-height:1.6">
Adjunto encontrará el reporte completo en PDF con todos los detalles del trabajo realizado.
</p>
{'<div style="text-align:center;margin-top:20px"><a href="' + pdf_link + '" style="background:#00b4d8;color:white;padding:12px 28px;border-radius:8px;text-decoration:none;font-weight:700;font-size:13px">Ver Reporte Online</a></div>' if pdf_link else ''}
</div>
<div style="background:#f0f4f8;padding:16px 30px;text-align:center;font-size:11px;color:#8a9bb0">
{company_name} · Reporte generado automáticamente por Marine Maintenance Pro
</div>
</div>
</body></html>"""
def send_wo_notification(company_id, to_emails, subject, body_html, pdf_path=None, pdf_name=None):
cfg = get_email_config(company_id)
if not cfg or not cfg.get('smtp_host'):
return False, "Sin configuración SMTP"
if isinstance(to_emails, str):
to_emails = [e.strip() for e in to_emails.replace(',',';').split(';') if e.strip()]
host = cfg['smtp_host']
port = int(cfg.get('smtp_port', 587))
user = cfg['smtp_user']
pwd = cfg['smtp_password']
from_addr = cfg.get('from_email') or user
from_name = cfg.get('from_name', '')
use_tls = bool(cfg.get('use_tls', 1))
msg = MIMEMultipart()
msg['From'] = f"{from_name} <{from_addr}>" if from_name else from_addr
msg['To'] = ', '.join(to_emails)
msg['Subject'] = subject
msg.attach(MIMEText(body_html, 'html', 'utf-8'))
if pdf_path and os.path.exists(pdf_path):
with open(pdf_path, 'rb') as f:
part = MIMEApplication(f.read(), _subtype='pdf')
part.add_header('Content-Disposition', 'attachment',
filename=pdf_name or os.path.basename(pdf_path))
msg.attach(part)
try:
_smtp_send(host, port, user, pwd, use_tls, from_addr, to_emails + [from_addr], msg.as_string())
return True, "OK"
except smtplib.SMTPAuthenticationError as e:
return False, f"Error de autenticación: {e}"
except smtplib.SMTPConnectError as e:
return False, f"No se pudo conectar a {host}:{port}: {e}"
except smtplib.SMTPServerDisconnected as e:
return False, f"Servidor cerró la conexión: {e}"
except OSError as e:
return False, f"Error de red: {e}"
except Exception as e:
return False, f"{type(e).__name__}: {e}"