diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 7ee11c7..489dd01 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -18,7 +18,7 @@ services: DATABASE_URL: "sqlite+aiosqlite:///./data/sage_dataven.db" restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + test: ["CMD", "curl", "-f", "http://localhost:8000/"] interval: 30s timeout: 10s retries: 3 \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 027eaf7..0908ff1 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -16,7 +16,7 @@ services: DATABASE_URL: "sqlite+aiosqlite:///./data/sage_prod.db" restart: always healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8004/health"] + test: ["CMD", "curl", "-f", "http://localhost:8004/"] interval: 30s timeout: 10s retries: 5 diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 81f9215..5bf1b18 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -16,7 +16,7 @@ services: DATABASE_URL: "sqlite+aiosqlite:///./data/sage_staging.db" restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8002/health"] + test: ["CMD", "curl", "-f", "http://localhost:8002/"] interval: 30s timeout: 10s retries: 3 \ No newline at end of file diff --git a/email_queue.py b/email_queue.py index 6079ae8..07fb3c5 100644 --- a/email_queue.py +++ b/email_queue.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta import smtplib import ssl import socket +import os from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.mime.application import MIMEApplication @@ -12,14 +13,67 @@ from config.config import settings import logging from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont from io import BytesIO from reportlab.lib.units import mm -from reportlab.lib.colors import HexColor +from reportlab.lib.colors import HexColor, Color +from PIL import Image logger = logging.getLogger(__name__) +FONT_PATH = os.getenv("SAGE_FONT_PATH", "/sage/pdfs/Sage_Text-Medium.ttf") +LOGO_PATH = os.getenv("SAGE_LOGO_PATH", "/sage/pdfs/logo.png") + +FONT_FALLBACK_PATHS = [ + "/sage/assets/Sage_Text-Medium.ttf", + "./sage/assets/Sage_Text-Medium.ttf", + "/sage/assets/Sage_Text-Bold.ttf", + "./sage/assets/Sage_Text-Bold.ttf", +] + +LOGO_FALLBACK_PATHS = [ + "/sage/assets/logo.png", + "./sage/assets/logo.png", +] + +_sage_font_registered = False + + +def _find_file(primary_path: str, fallback_paths: list) -> str | None: + """Recherche un fichier dans les chemins possibles.""" + all_paths = [primary_path] + fallback_paths + for path in all_paths: + if path and os.path.exists(path): + return path + return None + + +def _register_sage_font(): + """Enregistre la police Sage si disponible.""" + global _sage_font_registered + + if _sage_font_registered: + return True + + font_path = _find_file(FONT_PATH, FONT_FALLBACK_PATHS) + + if font_path: + try: + pdfmetrics.registerFont(TTFont("SageText", font_path)) + pdfmetrics.registerFont(TTFont("SageText-Bold", font_path)) + _sage_font_registered = True + logger.info(f"Police Sage enregistrée: {font_path}") + return True + except Exception as e: + logger.warning(f"Impossible d'enregistrer la police Sage: {e}") + + logger.info("Utilisation de Helvetica comme police de fallback") + return False + + class EmailQueue: def __init__(self): self.queue = queue.Queue() @@ -205,252 +259,604 @@ class EmailQueue: if not doc: raise Exception(f"Document {doc_id} introuvable") - buffer = BytesIO() - pdf = canvas.Canvas(buffer, pagesize=A4) - width, height = A4 + # Générer le PDF avec le nouveau générateur + generator = SagePDFGenerator(doc, type_doc) + return generator.generate() - # 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 +class SagePDFGenerator: + # Couleurs Sage + SAGE_GREEN = HexColor("#00D639") + SAGE_GREEN_DARK = HexColor("#00B830") + GRAY_50 = HexColor("#F9FAFB") + GRAY_100 = HexColor("#F3F4F6") + GRAY_200 = HexColor("#E5E7EB") + GRAY_300 = HexColor("#D1D5DB") + GRAY_400 = HexColor("#9CA3AF") + GRAY_500 = HexColor("#6B7280") + GRAY_600 = HexColor("#4B5563") + GRAY_700 = HexColor("#374151") + GRAY_800 = HexColor("#1F2937") + GRAY_900 = HexColor("#111827") - y = height - margin + COMPANY_NAME = "Bijou S.A.S" + COMPANY_ADDRESS_1 = "123 Avenue de la République" + COMPANY_ADDRESS_2 = "75011 Paris, France" + COMPANY_EMAIL = "contact@bijou.com" - # ===== HEADER ===== - y -= 20 * mm + # Labels des types de documents + TYPE_LABELS = { + 0: "Devis", + 10: "Commande", + 30: "Bon de Livraison", + 50: "Avoir", + 60: "Facture", + } - # Logo/Nom entreprise à gauche - pdf.setFont("Helvetica-Bold", 18) - pdf.setFillColor(green_color) - pdf.drawString(margin, y, "Bijou S.A.S") + def __init__(self, doc_data: dict, type_doc: int): + self.doc = doc_data + self.type_doc = type_doc + self.type_label = self.TYPE_LABELS.get(type_doc, "Document") - # 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()) + # Configuration de la page + self.page_width, self.page_height = A4 + self.margin = 15 * mm + self.content_width = self.page_width - 2 * self.margin - y -= 7 * mm - pdf.setFont("Helvetica", 9) - pdf.setFillColor(gray_600) + # État du générateur + self.buffer = BytesIO() + self.pdf = None + self.current_y = 0 + self.page_number = 1 + self.total_pages = 1 # Sera calculé - date_str = doc.get("date") or datetime.now().strftime("%d/%m/%Y") - pdf.drawRightString(width - margin, y, f"Date : {date_str}") + # Initialiser les polices + _register_sage_font() + self.use_sage_font = _sage_font_registered - y -= 5 * mm - date_livraison = ( - doc.get("date_livraison") or doc.get("date_echeance") or date_str + def _get_font(self, bold: bool = False) -> str: + """Retourne le nom de la police à utiliser.""" + if self.use_sage_font: + return "SageText-Bold" if bold else "SageText" + return "Helvetica-Bold" if bold else "Helvetica" + + def _draw_text( + self, + x: float, + y: float, + text: str, + font_size: int = 9, + bold: bool = False, + color: Color = None, + align: str = "left", + ): + self.pdf.setFont(self._get_font(bold), font_size) + + if color: + self.pdf.setFillColor(color) + + if align == "right": + self.pdf.drawRightString(x, y, str(text)) + elif align == "center": + self.pdf.drawCentredString(x, y, str(text)) + else: + self.pdf.drawString(x, y, str(text)) + + def _draw_logo( + self, + x: float, + y: float, + max_width: float = 50 * mm, + max_height: float = 20 * mm, + ): + """Dessine le logo Sage ou le nom de l'entreprise.""" + logo_path = _find_file(LOGO_PATH, LOGO_FALLBACK_PATHS) + + if logo_path: + try: + with Image.open(logo_path) as img: + img_width, img_height = img.size + + ratio = min(max_width / img_width, max_height / img_height) + draw_width = img_width * ratio + draw_height = img_height * ratio + + self.pdf.drawImage( + logo_path, + x, + y - draw_height, + width=draw_width, + height=draw_height, + preserveAspectRatio=True, + mask="auto", + ) + return draw_height + + except Exception as e: + logger.warning(f"Impossible de charger le logo: {e}") + + self._draw_text( + x, y - 8 * mm, "Sage", font_size=24, bold=True, color=self.SAGE_GREEN ) - pdf.drawRightString(width - margin, y, f"Validité : {date_livraison}") + return 10 * mm - y -= 5 * mm - reference = doc.get("reference") or "—" - pdf.drawRightString(width - margin, y, f"Réf : {reference}") + def _new_page(self): + """Crée une nouvelle page.""" + if self.pdf: + self.pdf.showPage() + self.page_number += 1 + self.current_y = self.page_height - self.margin - # ===== ADDRESSES ===== - y -= 20 * mm + def _draw_header(self): + y = self.page_height - self.margin - # Émetteur (gauche) - col1_x = margin - col2_x = margin + content_width / 2 + 6 * mm - col_width = content_width / 2 - 6 * mm + logo_height = self._draw_logo( + self.margin, y, max_width=55 * mm, max_height=22 * mm + ) - pdf.setFont("Helvetica-Bold", 8) - pdf.setFillColor(gray_400) - pdf.drawString(col1_x, y, "ÉMETTEUR") + right_x = self.page_width - self.margin - y_emetteur = y - 5 * mm - pdf.setFont("Helvetica-Bold", 10) - pdf.setFillColor(gray_800) - pdf.drawString(col1_x, y_emetteur, "Bijou S.A.S") + numero = self.doc.get("numero") or "BROUILLON" + self._draw_text( + right_x, + y - 6 * mm, + numero.upper(), + font_size=20, + bold=True, + color=self.GRAY_800, + align="right", + ) + + date_str = self.doc.get("date") or datetime.now().strftime("%d/%m/%Y") + self._draw_text( + right_x, + y - 14 * mm, + f"Date : {date_str}", + font_size=9, + color=self.GRAY_600, + align="right", + ) + + date_validite = ( + self.doc.get("date_livraison") or self.doc.get("date_echeance") or date_str + ) + self._draw_text( + right_x, + y - 20 * mm, + f"Validité : {date_validite}", + font_size=9, + color=self.GRAY_600, + align="right", + ) + + reference = self.doc.get("reference") or "—" + self._draw_text( + right_x, + y - 26 * mm, + f"Réf : {reference}", + font_size=9, + color=self.GRAY_600, + align="right", + ) + + return y - max(logo_height + 10 * mm, 35 * mm) + + def _draw_addresses(self, y: float) -> float: + col1_x = self.margin + col2_x = self.margin + self.content_width / 2 + 8 * mm + col_width = self.content_width / 2 - 8 * mm + + self._draw_text( + col1_x, y, "ÉMETTEUR", font_size=8, bold=True, color=self.GRAY_400 + ) + + y_emetteur = y - 6 * mm + self._draw_text( + col1_x, + y_emetteur, + self.COMPANY_NAME, + font_size=10, + bold=True, + color=self.GRAY_800, + ) y_emetteur -= 5 * mm - pdf.setFont("Helvetica", 9) - pdf.setFillColor(gray_600) - pdf.drawString(col1_x, y_emetteur, "123 Avenue de la République") + self._draw_text( + col1_x, y_emetteur, self.COMPANY_ADDRESS_1, font_size=9, color=self.GRAY_600 + ) 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 + self._draw_text( + col1_x, y_emetteur, self.COMPANY_ADDRESS_2, font_size=9, color=self.GRAY_600 ) - pdf.setFillColor(gray_400) - pdf.setFont("Helvetica-Bold", 8) - pdf.drawString(col2_x + 4 * mm, y, "DESTINATAIRE") + y_emetteur -= 5 * mm + self._draw_text( + col1_x, y_emetteur, self.COMPANY_EMAIL, font_size=9, color=self.GRAY_600 + ) - 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) + box_padding = 4 * mm + box_height = 26 * mm + box_y = y - 3 * mm + + # Fond gris arrondi + self.pdf.setFillColor(self.GRAY_50) + self.pdf.roundRect( + col2_x - box_padding, + box_y - box_height, + col_width + 2 * box_padding, + box_height + box_padding, + 3 * mm, + fill=1, + stroke=0, + ) + + self._draw_text( + col2_x, y, "DESTINATAIRE", font_size=8, bold=True, color=self.GRAY_400 + ) + + y_dest = y - 6 * mm + client_name = ( + self.doc.get("client_intitule") or self.doc.get("client_nom") or "Client" + ) + self._draw_text( + col2_x, y_dest, client_name, font_size=10, bold=True, color=self.GRAY_800 + ) 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 "" + client_adresse = ( + self.doc.get("client_adresse") or self.doc.get("adresse_livraison") or "" + ) + if client_adresse: + if len(client_adresse) > 40: + client_adresse = client_adresse[:37] + "..." + self._draw_text( + col2_x, y_dest, client_adresse, font_size=9, color=self.GRAY_600 ) - if len(designation) > 60: - designation = designation[:57] + "..." + y_dest -= 4 * mm - pdf.setFillColor(gray_800) - pdf.setFont("Helvetica-Bold", 8) - pdf.drawString(col_designation, y, designation) + client_cp = self.doc.get("client_cp") or "" + client_ville = self.doc.get("client_ville") or "" + if client_cp or client_ville: + ville_complete = f"{client_cp} {client_ville}".strip() + self._draw_text( + col2_x, y_dest, ville_complete, font_size=9, color=self.GRAY_600 + ) + return min(y_emetteur, box_y - box_height) - 15 * mm + + def _draw_table_header(self, y: float) -> float: + col_des = self.margin + col_qte = self.margin + 95 * mm + col_pu = self.margin + 115 * mm + col_rem = self.margin + 140 * mm + col_tva = self.margin + 157 * mm + col_mt = self.page_width - self.margin + + self.pdf.setStrokeColor(self.GRAY_200) + self.pdf.setLineWidth(0.5) + self.pdf.line( + self.margin, y - 3 * mm, self.page_width - self.margin, y - 3 * mm + ) + + self._draw_text( + col_des, y, "Désignation", font_size=9, bold=True, color=self.GRAY_700 + ) + self._draw_text( + col_qte, + y, + "Qté", + font_size=9, + bold=True, + color=self.GRAY_700, + align="right", + ) + self._draw_text( + col_pu, + y, + "Prix Unit. HT", + font_size=9, + bold=True, + color=self.GRAY_700, + align="right", + ) + self._draw_text( + col_rem, + y, + "Remise", + font_size=9, + bold=True, + color=self.GRAY_700, + align="right", + ) + self._draw_text( + col_tva, + y, + "TVA", + font_size=9, + bold=True, + color=self.GRAY_700, + align="right", + ) + self._draw_text( + col_mt, + y, + "Montant HT", + font_size=9, + bold=True, + color=self.GRAY_700, + align="right", + ) + + return y - 8 * mm + + def _draw_line_item(self, y: float, ligne: dict, alternate: bool = False) -> float: + col_des = self.margin + col_qte = self.margin + 95 * mm + col_pu = self.margin + 115 * mm + col_rem = self.margin + 140 * mm + col_tva = self.margin + 157 * mm + col_mt = self.page_width - self.margin + + if alternate: + self.pdf.setFillColor(self.GRAY_50) + self.pdf.rect( + self.margin - 2 * mm, + y - 5 * mm, + self.content_width + 4 * mm, + 12 * mm, + fill=1, + stroke=0, + ) + + designation = ( + ligne.get("designation") + or ligne.get("designation_article") + or ligne.get("article_designation") + or "" + ) + if len(designation) > 50: + designation = designation[:47] + "..." + self._draw_text( + col_des, y, designation, font_size=9, bold=True, color=self.GRAY_800 + ) + + description = ligne.get("description", "") + line_height = 6 * mm + + if description and description != designation: y -= 4 * mm + if len(description) > 60: + description = description[:57] + "..." + self._draw_text(col_des, y, description, font_size=8, color=self.GRAY_500) + line_height += 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 + quantite = ligne.get("quantite") or 0 + self._draw_text( + col_qte, + y + (4 * mm if description and description != designation else 0), + str(quantite), + font_size=9, + color=self.GRAY_800, + align="right", + ) - # Valeurs - y += 4 * mm # Remonter pour aligner avec la désignation - pdf.setFont("Helvetica", 8) - pdf.setFillColor(gray_800) + prix_unit = ligne.get("prix_unitaire_ht") or ligne.get("prix_unitaire") or 0 + self._draw_text( + col_pu, + y + (4 * mm if description and description != designation else 0), + f"{prix_unit:.2f} €", + font_size=9, + color=self.GRAY_800, + align="right", + ) - quantite = ligne.get("quantite") or 0 - pdf.drawRightString(col_quantite, y, str(quantite)) + remise = ligne.get("remise_pourcentage") or ligne.get("remise") or 0 + remise_text = f"{remise:.0f}%" if remise > 0 else "—" + remise_color = self.SAGE_GREEN_DARK if remise > 0 else self.GRAY_400 + self._draw_text( + col_rem, + y + (4 * mm if description and description != designation else 0), + remise_text, + font_size=9, + color=remise_color, + align="right", + ) - prix_unit = ligne.get("prix_unitaire_ht") or ligne.get("prix_unitaire", 0) - pdf.drawRightString(col_prix_unit, y, f"{prix_unit:.2f} €") + # TVA + taux_tva = ligne.get("taux_taxe1") or ligne.get("taux_tva") or 20 + self._draw_text( + col_tva, + y + (4 * mm if description and description != designation else 0), + f"{taux_tva:.0f}%", + font_size=9, + color=self.GRAY_600, + align="right", + ) - taux_taxe = ligne.get("taux_taxe1") or 20 - pdf.drawRightString(col_taux_taxe, y, f"{taux_taxe}%") + montant_ht = ligne.get("montant_ligne_ht") or ligne.get("montant_ht") or 0 + self._draw_text( + col_mt, + y + (4 * mm if description and description != designation else 0), + f"{montant_ht:.2f} €", + font_size=9, + bold=True, + color=self.GRAY_800, + align="right", + ) - 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} €") + return y - line_height - 2 * mm - y -= 8 * mm + def _draw_totals(self, y: float) -> float: + totals_x = self.page_width - self.margin - 60 * mm + right_x = self.page_width - self.margin - # 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 + self.pdf.setStrokeColor(self.GRAY_200) + self.pdf.setLineWidth(0.5) + self.pdf.line(totals_x - 5 * mm, y + 5 * mm, right_x, y + 5 * 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} €") + self._draw_text(totals_x, y, "Total HT", font_size=9, color=self.GRAY_600) + total_ht = self.doc.get("total_ht_net") or self.doc.get("total_ht") or 0 + self._draw_text( + right_x, + y, + f"{total_ht:.2f} €", + font_size=9, + color=self.GRAY_800, + align="right", + ) y -= 6 * mm + remise_globale = ( + self.doc.get("remise_globale") or self.doc.get("montant_remise") or 0 + ) + if remise_globale > 0: + self._draw_text(totals_x, y, "Remise", font_size=9, color=self.GRAY_600) + self._draw_text( + right_x, + y, + f"-{remise_globale:.2f} €", + font_size=9, + color=self.SAGE_GREEN_DARK, + align="right", + ) + 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} €") + total_ttc = self.doc.get("total_ttc") or 0 + tva = total_ttc - total_ht + remise_globale + taux_tva_global = self.doc.get("taux_tva_principal") or 20 + self._draw_text( + totals_x, + y, + f"TVA ({taux_tva_global:.0f}%)", + font_size=9, + color=self.GRAY_600, + ) + self._draw_text( + right_x, y, f"{tva:.2f} €", font_size=9, color=self.GRAY_800, align="right" + ) - y -= 8 * mm + y -= 10 * mm - # Ligne de séparation - pdf.setStrokeColor(gray_400) - pdf.line(totals_x, y + 2 * mm, width - margin, y + 2 * mm) + self.pdf.setStrokeColor(self.GRAY_300) + self.pdf.setLineWidth(1) + self.pdf.line(totals_x - 5 * mm, y + 4 * mm, right_x, y + 4 * 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} €") + self._draw_text( + totals_x, y, "Net à payer", font_size=12, bold=True, color=self.SAGE_GREEN + ) + self._draw_text( + right_x, + y, + f"{total_ttc:.2f} €", + font_size=12, + bold=True, + color=self.SAGE_GREEN, + align="right", + ) - # ===== 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) + return y - 15 * mm - y -= 5 * mm - pdf.setFont("Helvetica-Bold", 8) - pdf.setFillColor(gray_400) - pdf.drawString(margin, y, "NOTES & CONDITIONS") + def _draw_notes(self, y: float) -> float: + notes = ( + self.doc.get("notes_publique") + or self.doc.get("notes") + or self.doc.get("commentaire") + ) - y -= 5 * mm - pdf.setFont("Helvetica", 8) - pdf.setFillColor(gray_600) + if not notes: + return y - # 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]) + self.pdf.setStrokeColor(self.GRAY_200) + self.pdf.setLineWidth(0.5) + self.pdf.line( + self.margin, y + 5 * mm, self.page_width - self.margin, y + 5 * mm + ) + + self._draw_text( + self.margin, + y, + "NOTES & CONDITIONS", + font_size=8, + bold=True, + color=self.GRAY_400, + ) + + y -= 5 * mm + + for line in notes.split("\n"): + if y < 30 * mm: + break + line = line.strip() + if line: + if len(line) > 90: + line = line[:87] + "..." + self._draw_text(self.margin, y, line, font_size=8, color=self.GRAY_600) y -= 4 * mm - # ===== FOOTER ===== - pdf.setFont("Helvetica", 7) - pdf.setFillColor(gray_400) - pdf.drawCentredString(width / 2, 15 * mm, "Page 1 / 1") + return y - pdf.save() - buffer.seek(0) + def _draw_footer(self): + footer_y = 12 * mm - return buffer.read() + page_text = f"Page {self.page_number} / {self.total_pages}" + self._draw_text( + self.page_width / 2, + footer_y, + page_text, + font_size=8, + color=self.GRAY_400, + align="center", + ) + + def generate(self) -> bytes: + self.pdf = canvas.Canvas(self.buffer, pagesize=A4) + + self.current_y = self._draw_header() + + self.current_y = self._draw_addresses(self.current_y) + + self.current_y = self._draw_table_header(self.current_y) + + lignes = self.doc.get("lignes", []) + + if not lignes: + self.pdf.setFont(self._get_font(), 9) + self.pdf.setFillColor(self.GRAY_400) + self.pdf.drawCentredString( + self.page_width / 2, self.current_y, "Aucune ligne" + ) + self.current_y -= 15 * mm + else: + for idx, ligne in enumerate(lignes): + if self.current_y < 70 * mm: + self._draw_footer() + self._new_page() + self.current_y = self._draw_table_header( + self.page_height - self.margin - 10 * mm + ) + + self.current_y = self._draw_line_item( + self.current_y, ligne, alternate=(idx % 2 == 1) + ) + + if self.current_y < 60 * mm: + self._draw_footer() + self._new_page() + self.current_y = self.page_height - self.margin - 20 * mm + + self.current_y = self._draw_totals(self.current_y) + + self.current_y = self._draw_notes(self.current_y) + + self._draw_footer() + + self.pdf.save() + self.buffer.seek(0) + + return self.buffer.read() email_queue = EmailQueue() diff --git a/sage/pdfs/Sage_Text-Bold.ttf b/sage/pdfs/Sage_Text-Bold.ttf new file mode 100644 index 0000000..249ee85 Binary files /dev/null and b/sage/pdfs/Sage_Text-Bold.ttf differ diff --git a/sage/pdfs/Sage_Text-Medium.ttf b/sage/pdfs/Sage_Text-Medium.ttf new file mode 100644 index 0000000..a83b7a9 Binary files /dev/null and b/sage/pdfs/Sage_Text-Medium.ttf differ diff --git a/sage/pdfs/logo.png b/sage/pdfs/logo.png new file mode 100644 index 0000000..f3a61f6 Binary files /dev/null and b/sage/pdfs/logo.png differ diff --git a/services/universign_document.py b/services/universign_document.py index 60e46d3..fa899a9 100644 --- a/services/universign_document.py +++ b/services/universign_document.py @@ -36,7 +36,7 @@ class UniversignDocumentService: data = response.json() documents = data.get("documents", []) - logger.info(f"✅ {len(documents)} document(s) trouvé(s)") + logger.info(f"{len(documents)} document(s) trouvé(s)") # Log détaillé de chaque document for idx, doc in enumerate(documents): @@ -90,7 +90,7 @@ class UniversignDocumentService: content_length = response.headers.get("Content-Length", "unknown") logger.info( - f"✅ Téléchargement réussi: " + f"Téléchargement réussi: " f"Content-Type={content_type}, Size={content_length}" ) @@ -100,7 +100,7 @@ class UniversignDocumentService: and "octet-stream" not in content_type.lower() ): logger.warning( - f"⚠️ Type de contenu inattendu: {content_type}. " + f"Type de contenu inattendu: {content_type}. " f"Tentative de lecture quand même..." ) @@ -146,7 +146,7 @@ class UniversignDocumentService: if not force and transaction.signed_document_path: if os.path.exists(transaction.signed_document_path): logger.debug( - f"✅ Document déjà téléchargé: {transaction.transaction_id}" + f"Document déjà téléchargé: {transaction.transaction_id}" ) return True, None @@ -162,7 +162,7 @@ class UniversignDocumentService: if not documents: error = "Aucun document trouvé dans la transaction Universign" - logger.warning(f"⚠️ {error}") + logger.warning(f"{error}") transaction.download_error = error await session.commit() return False, error @@ -232,7 +232,7 @@ class UniversignDocumentService: await session.commit() logger.info( - f"✅ Document signé téléchargé: {filename} ({file_size / 1024:.1f} KB)" + f"Document signé téléchargé: {filename} ({file_size / 1024:.1f} KB)" ) return True, None diff --git a/services/universign_sync.py b/services/universign_sync.py index 2390023..28e633c 100644 --- a/services/universign_sync.py +++ b/services/universign_sync.py @@ -372,7 +372,7 @@ class UniversignSyncService: # Vérifier la transition if not is_transition_allowed(previous_local_status, new_local_status): logger.warning( - f"⚠️ Transition refusée: {previous_local_status} → {new_local_status}" + f"Transition refusée: {previous_local_status} → {new_local_status}" ) new_local_status = resolve_status_conflict( previous_local_status, new_local_status @@ -392,7 +392,7 @@ class UniversignSyncService: universign_status_raw ) except ValueError: - logger.warning(f"⚠️ Statut Universign inconnu: {universign_status_raw}") + logger.warning(f"Statut Universign inconnu: {universign_status_raw}") if new_local_status == "SIGNE": transaction.universign_status = ( UniversignTransactionStatus.COMPLETED @@ -415,7 +415,7 @@ class UniversignSyncService: if new_local_status == "SIGNE" and not transaction.signed_at: transaction.signed_at = datetime.now() - logger.info("✅ Date de signature mise à jour") + logger.info("Date de signature mise à jour") if new_local_status == "REFUSE" and not transaction.refused_at: transaction.refused_at = datetime.now() @@ -450,9 +450,9 @@ class UniversignSyncService: ) if download_success: - logger.info("✅ Document signé téléchargé et stocké") + logger.info("Document signé téléchargé et stocké") else: - logger.warning(f"⚠️ Échec téléchargement: {download_error}") + logger.warning(f"Échec téléchargement: {download_error}") except Exception as e: logger.error( @@ -499,7 +499,7 @@ class UniversignSyncService: ) logger.info( - f"✅ Sync terminée: {transaction.transaction_id} | " + f"Sync terminée: {transaction.transaction_id} | " f"{previous_local_status} → {new_local_status}" ) @@ -557,9 +557,9 @@ class UniversignSyncService: ) if download_success: - logger.info("✅ Document signé téléchargé avec succès") + logger.info("Document signé téléchargé avec succès") else: - logger.warning(f"⚠️ Échec téléchargement: {download_error}") + logger.warning(f"Échec téléchargement: {download_error}") except Exception as e: logger.error(