67a0e674ca
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>
211 lines
9.2 KiB
Python
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}"
|