Sage100-vps/email_queue.py

456 lines
14 KiB
Python

import threading
import queue
import asyncio
from datetime import datetime, timedelta
import smtplib
import ssl
import socket
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from config.config import settings
import logging
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from io import BytesIO
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor
logger = logging.getLogger(__name__)
class EmailQueue:
def __init__(self):
self.queue = queue.Queue()
self.workers = []
self.running = False
self.session_factory = None
self.sage_client = None
def start(self, num_workers: int = 3):
if self.running:
return
self.running = True
for i in range(num_workers):
worker = threading.Thread(
target=self._worker, name=f"EmailWorker-{i}", daemon=True
)
worker.start()
self.workers.append(worker)
logger.info(f"Queue email démarrée avec {num_workers} worker(s)")
def stop(self):
self.running = False
try:
self.queue.join()
except Exception:
pass
def enqueue(self, email_log_id: str):
self.queue.put(email_log_id)
def _worker(self):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
while self.running:
try:
email_log_id = self.queue.get(timeout=1)
loop.run_until_complete(self._process_email(email_log_id))
self.queue.task_done()
except queue.Empty:
continue
except Exception as e:
logger.error(f"Erreur worker: {e}")
try:
self.queue.task_done()
except Exception:
pass
finally:
loop.close()
async def _process_email(self, email_log_id: str):
from database import EmailLog, StatutEmail
from sqlalchemy import select
if not self.session_factory:
logger.error("session_factory non configuré")
return
async with self.session_factory() as session:
result = await session.execute(
select(EmailLog).where(EmailLog.id == email_log_id)
)
email_log = result.scalar_one_or_none()
if not email_log:
logger.error(f"Email log {email_log_id} introuvable")
return
email_log.statut = StatutEmail.EN_COURS
email_log.nb_tentatives += 1
await session.commit()
try:
await self._send_with_retry(email_log)
email_log.statut = StatutEmail.ENVOYE
email_log.date_envoi = datetime.now()
email_log.derniere_erreur = None
except Exception as e:
error_msg = str(e)
email_log.statut = StatutEmail.ERREUR
email_log.derniere_erreur = error_msg[:1000]
if email_log.nb_tentatives < settings.max_retry_attempts:
delay = settings.retry_delay_seconds * (
2 ** (email_log.nb_tentatives - 1)
)
email_log.prochain_retry = datetime.now() + timedelta(seconds=delay)
timer = threading.Timer(delay, self.enqueue, args=[email_log_id])
timer.daemon = True
timer.start()
await session.commit()
async def _send_with_retry(self, email_log):
msg = MIMEMultipart()
msg["From"] = settings.smtp_from
msg["To"] = email_log.destinataire
msg["Subject"] = email_log.sujet
msg.attach(MIMEText(email_log.corps_html, "html"))
# Attachement des PDFs
if email_log.document_ids:
document_ids = email_log.document_ids.split(",")
type_doc = email_log.type_document
for doc_id in document_ids:
doc_id = doc_id.strip()
if not doc_id:
continue
try:
pdf_bytes = await asyncio.to_thread(
self._generate_pdf, doc_id, type_doc
)
if pdf_bytes:
part = MIMEApplication(pdf_bytes, Name=f"{doc_id}.pdf")
part["Content-Disposition"] = (
f'attachment; filename="{doc_id}.pdf"'
)
msg.attach(part)
except Exception as e:
logger.error(f"Erreur génération PDF {doc_id}: {e}")
# Envoi SMTP
await asyncio.to_thread(self._send_smtp, msg)
def _send_smtp(self, msg):
server = None
try:
# Résolution DNS
socket.getaddrinfo(settings.smtp_host, settings.smtp_port)
# Connexion
server = smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=30)
# EHLO
server.ehlo()
# STARTTLS
if settings.smtp_use_tls:
if server.has_extn("STARTTLS"):
context = ssl.create_default_context()
server.starttls(context=context)
server.ehlo()
# Authentification
if settings.smtp_user and settings.smtp_password:
server.login(settings.smtp_user, settings.smtp_password)
# Envoi
refused = server.send_message(msg)
if refused:
raise Exception(f"Destinataires refusés: {refused}")
# Fermeture
server.quit()
except Exception as e:
if server:
try:
server.quit()
except Exception:
pass
raise Exception(f"Erreur SMTP: {str(e)}")
def _generate_pdf(self, doc_id: str, type_doc: int) -> bytes:
if not self.sage_client:
raise Exception("sage_client non disponible")
try:
doc = self.sage_client.lire_document(doc_id, type_doc)
except Exception as e:
raise Exception(f"Document {doc_id} inaccessible : {e}")
if not doc:
raise Exception(f"Document {doc_id} introuvable")
buffer = BytesIO()
pdf = canvas.Canvas(buffer, pagesize=A4)
width, height = A4
# Couleurs
green_color = HexColor("#2A6F4F")
gray_400 = HexColor("#9CA3AF")
gray_600 = HexColor("#4B5563")
gray_800 = HexColor("#1F2937")
# Marges
margin = 8 * mm
content_width = width - 2 * margin
y = height - margin
# ===== HEADER =====
y -= 20 * mm
# Logo/Nom entreprise à gauche
pdf.setFont("Helvetica-Bold", 18)
pdf.setFillColor(green_color)
pdf.drawString(margin, y, "Bijou S.A.S")
# Informations document à droite
pdf.setFillColor(gray_800)
pdf.setFont("Helvetica-Bold", 20)
numero = doc.get("numero") or "BROUILLON"
pdf.drawRightString(width - margin, y, numero.upper())
y -= 7 * mm
pdf.setFont("Helvetica", 9)
pdf.setFillColor(gray_600)
date_str = doc.get("date") or datetime.now().strftime("%d/%m/%Y")
pdf.drawRightString(width - margin, y, f"Date : {date_str}")
y -= 5 * mm
date_livraison = (
doc.get("date_livraison") or doc.get("date_echeance") or date_str
)
pdf.drawRightString(width - margin, y, f"Validité : {date_livraison}")
y -= 5 * mm
reference = doc.get("reference") or ""
pdf.drawRightString(width - margin, y, f"Réf : {reference}")
# ===== ADDRESSES =====
y -= 20 * mm
# Émetteur (gauche)
col1_x = margin
col2_x = margin + content_width / 2 + 6 * mm
col_width = content_width / 2 - 6 * mm
pdf.setFont("Helvetica-Bold", 8)
pdf.setFillColor(gray_400)
pdf.drawString(col1_x, y, "ÉMETTEUR")
y_emetteur = y - 5 * mm
pdf.setFont("Helvetica-Bold", 10)
pdf.setFillColor(gray_800)
pdf.drawString(col1_x, y_emetteur, "Bijou S.A.S")
y_emetteur -= 5 * mm
pdf.setFont("Helvetica", 9)
pdf.setFillColor(gray_600)
pdf.drawString(col1_x, y_emetteur, "123 Avenue de la République")
y_emetteur -= 4 * mm
pdf.drawString(col1_x, y_emetteur, "75011 Paris, France")
y_emetteur -= 5 * mm
pdf.drawString(col1_x, y_emetteur, "contact@bijou.com")
# Destinataire (droite, avec fond gris)
box_y = y - 4 * mm
box_height = 28 * mm
pdf.setFillColorRGB(0.97, 0.97, 0.97) # bg-gray-50
pdf.roundRect(
col2_x, box_y - box_height, col_width, box_height, 3 * mm, fill=1, stroke=0
)
pdf.setFillColor(gray_400)
pdf.setFont("Helvetica-Bold", 8)
pdf.drawString(col2_x + 4 * mm, y, "DESTINATAIRE")
y_dest = y - 5 * mm
pdf.setFont("Helvetica-Bold", 10)
pdf.setFillColor(gray_800)
client_name = doc.get("client_intitule") or "Client"
pdf.drawString(col2_x + 4 * mm, y_dest, client_name)
y_dest -= 5 * mm
pdf.setFont("Helvetica", 9)
pdf.setFillColor(gray_600)
pdf.drawString(col2_x + 4 * mm, y_dest, "10 rue des Clients")
y_dest -= 4 * mm
pdf.drawString(col2_x + 4 * mm, y_dest, "75001 Paris")
# ===== LIGNES D'ARTICLES =====
y = min(y_emetteur, y_dest) - 20 * mm
# En-têtes des colonnes
col_designation = margin
col_quantite = width - margin - 80 * mm
col_prix_unit = width - margin - 64 * mm
col_taux_taxe = width - margin - 40 * mm
col_montant = width - margin - 24 * mm
pdf.setFont("Helvetica-Bold", 9)
pdf.setFillColor(gray_800)
pdf.drawString(col_designation, y, "Désignation")
pdf.drawRightString(col_quantite, y, "Qté")
pdf.drawRightString(col_prix_unit, y, "Prix Unit. HT")
pdf.drawRightString(col_taux_taxe, y, "TVA")
pdf.drawRightString(col_montant, y, "Montant HT")
y -= 7 * mm
# Lignes d'articles
pdf.setFont("Helvetica", 8)
lignes = doc.get("lignes", [])
for ligne in lignes:
if y < 60 * mm: # Nouvelle page si nécessaire
pdf.showPage()
y = height - margin - 20 * mm
pdf.setFont("Helvetica", 8)
designation = (
ligne.get("designation") or ligne.get("designation_article") or ""
)
if len(designation) > 60:
designation = designation[:57] + "..."
pdf.setFillColor(gray_800)
pdf.setFont("Helvetica-Bold", 8)
pdf.drawString(col_designation, y, designation)
y -= 4 * mm
# Description (si différente)
description = ligne.get("description", "")
if description and description != designation:
pdf.setFont("Helvetica", 7)
pdf.setFillColor(gray_600)
if len(description) > 70:
description = description[:67] + "..."
pdf.drawString(col_designation, y, description)
y -= 4 * mm
# Valeurs
y += 4 * mm # Remonter pour aligner avec la désignation
pdf.setFont("Helvetica", 8)
pdf.setFillColor(gray_800)
quantite = ligne.get("quantite") or 0
pdf.drawRightString(col_quantite, y, str(quantite))
prix_unit = ligne.get("prix_unitaire_ht") or ligne.get("prix_unitaire", 0)
pdf.drawRightString(col_prix_unit, y, f"{prix_unit:.2f}")
taux_taxe = ligne.get("taux_taxe1") or 20
pdf.drawRightString(col_taux_taxe, y, f"{taux_taxe}%")
montant = ligne.get("montant_ligne_ht") or ligne.get("montant_ht", 0)
pdf.setFont("Helvetica-Bold", 8)
pdf.drawRightString(col_montant, y, f"{montant:.2f}")
y -= 8 * mm
# Si aucune ligne
if not lignes:
pdf.setFont("Helvetica-Oblique", 9)
pdf.setFillColor(gray_400)
pdf.drawCentredString(width / 2, y, "Aucune ligne")
y -= 15 * mm
# ===== TOTAUX =====
y -= 10 * mm
totals_x = width - margin - 64 * mm
totals_label_width = 40 * mm
pdf.setFont("Helvetica", 9)
pdf.setFillColor(gray_600)
# Total HT
pdf.drawString(totals_x, y, "Total HT")
total_ht = doc.get("total_ht_net") or doc.get("total_ht") or 0
pdf.drawRightString(width - margin, y, f"{total_ht:.2f}")
y -= 6 * mm
# TVA
pdf.drawString(totals_x, y, "TVA")
total_ttc = doc.get("total_ttc") or 0
tva = total_ttc - total_ht
pdf.drawRightString(width - margin, y, f"{tva:.2f}")
y -= 8 * mm
# Ligne de séparation
pdf.setStrokeColor(gray_400)
pdf.line(totals_x, y + 2 * mm, width - margin, y + 2 * mm)
# Net à payer
pdf.setFont("Helvetica-Bold", 12)
pdf.setFillColor(green_color)
pdf.drawString(totals_x, y, "Net à payer")
pdf.drawRightString(width - margin, y, f"{total_ttc:.2f}")
# ===== NOTES =====
notes = doc.get("notes_publique") or doc.get("notes")
if notes:
y -= 15 * mm
pdf.setStrokeColor(gray_400)
pdf.line(margin, y + 5 * mm, width - margin, y + 5 * mm)
y -= 5 * mm
pdf.setFont("Helvetica-Bold", 8)
pdf.setFillColor(gray_400)
pdf.drawString(margin, y, "NOTES & CONDITIONS")
y -= 5 * mm
pdf.setFont("Helvetica", 8)
pdf.setFillColor(gray_600)
# Gérer les sauts de ligne dans les notes
for line in notes.split("\n"):
if y < 25 * mm:
break
pdf.drawString(margin, y, line[:100])
y -= 4 * mm
# ===== FOOTER =====
pdf.setFont("Helvetica", 7)
pdf.setFillColor(gray_400)
pdf.drawCentredString(width / 2, 15 * mm, "Page 1 / 1")
pdf.save()
buffer.seek(0)
return buffer.read()
email_queue = EmailQueue()