feat(pdf): refactor PDF generation with new SagePDFGenerator class

This commit is contained in:
Fanilo-Nantenaina 2026-01-13 16:30:34 +03:00
parent b17e4abf12
commit a2c85a211a
9 changed files with 618 additions and 212 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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",
)
pdf.drawRightString(width - margin, y, f"Validité : {date_livraison}")
return draw_height
y -= 5 * mm
reference = doc.get("reference") or ""
pdf.drawRightString(width - margin, y, f"Réf : {reference}")
except Exception as e:
logger.warning(f"Impossible de charger le logo: {e}")
# ===== ADDRESSES =====
y -= 20 * mm
self._draw_text(
x, y - 8 * mm, "Sage", font_size=24, bold=True, color=self.SAGE_GREEN
)
return 10 * mm
# Émetteur (gauche)
col1_x = margin
col2_x = margin + content_width / 2 + 6 * mm
col_width = content_width / 2 - 6 * mm
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
pdf.setFont("Helvetica-Bold", 8)
pdf.setFillColor(gray_400)
pdf.drawString(col1_x, y, "ÉMETTEUR")
def _draw_header(self):
y = self.page_height - 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")
logo_height = self._draw_logo(
self.margin, y, max_width=55 * mm, max_height=22 * mm
)
right_x = self.page_width - self.margin
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")
self._draw_text(
col1_x, y_emetteur, self.COMPANY_ADDRESS_2, font_size=9, color=self.GRAY_600
)
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_EMAIL, font_size=9, color=self.GRAY_600
)
pdf.setFillColor(gray_400)
pdf.setFont("Helvetica-Bold", 8)
pdf.drawString(col2_x + 4 * mm, y, "DESTINATAIRE")
box_padding = 4 * mm
box_height = 26 * mm
box_y = y - 3 * mm
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)
# 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")
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
)
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
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
)
# 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
return min(y_emetteur, box_y - box_height) - 15 * 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")
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
y -= 7 * mm
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
)
# Lignes d'articles
pdf.setFont("Helvetica", 8)
lignes = doc.get("lignes", [])
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",
)
for ligne in lignes:
if y < 60 * mm: # Nouvelle page si nécessaire
pdf.showPage()
y = height - margin - 20 * mm
pdf.setFont("Helvetica", 8)
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("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
)
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
line_height = 6 * mm
# Valeurs
y += 4 * mm # Remonter pour aligner avec la désignation
pdf.setFont("Helvetica", 8)
pdf.setFillColor(gray_800)
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
quantite = ligne.get("quantite") or 0
pdf.drawRightString(col_quantite, y, str(quantite))
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",
)
prix_unit = ligne.get("prix_unitaire_ht") or ligne.get("prix_unitaire", 0)
pdf.drawRightString(col_prix_unit, y, f"{prix_unit:.2f}")
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",
)
taux_taxe = ligne.get("taux_taxe1") or 20
pdf.drawRightString(col_taux_taxe, y, f"{taux_taxe}%")
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",
)
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}")
# 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",
)
y -= 8 * mm
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",
)
# 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
return y - line_height - 2 * mm
# ===== TOTAUX =====
y -= 10 * mm
def _draw_totals(self, y: float) -> float:
totals_x = self.page_width - self.margin - 60 * mm
right_x = self.page_width - self.margin
totals_x = width - margin - 64 * mm
totals_label_width = 40 * 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)
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
def _draw_notes(self, y: float) -> float:
notes = (
self.doc.get("notes_publique")
or self.doc.get("notes")
or self.doc.get("commentaire")
)
if not notes:
return y
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
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:
if y < 30 * mm:
break
pdf.drawString(margin, y, line[:100])
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()

Binary file not shown.

Binary file not shown.

BIN
sage/pdfs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -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

View file

@ -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(