feat(pdf): refactor PDF generation with new SagePDFGenerator class
This commit is contained in:
parent
b17e4abf12
commit
a2c85a211a
9 changed files with 618 additions and 212 deletions
|
|
@ -18,7 +18,7 @@ services:
|
||||||
DATABASE_URL: "sqlite+aiosqlite:///./data/sage_dataven.db"
|
DATABASE_URL: "sqlite+aiosqlite:///./data/sage_dataven.db"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
@ -16,7 +16,7 @@ services:
|
||||||
DATABASE_URL: "sqlite+aiosqlite:///./data/sage_prod.db"
|
DATABASE_URL: "sqlite+aiosqlite:///./data/sage_prod.db"
|
||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8004/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8004/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ services:
|
||||||
DATABASE_URL: "sqlite+aiosqlite:///./data/sage_staging.db"
|
DATABASE_URL: "sqlite+aiosqlite:///./data/sage_staging.db"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:8002/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
768
email_queue.py
768
email_queue.py
|
|
@ -5,6 +5,7 @@ from datetime import datetime, timedelta
|
||||||
import smtplib
|
import smtplib
|
||||||
import ssl
|
import ssl
|
||||||
import socket
|
import socket
|
||||||
|
import os
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.application import MIMEApplication
|
from email.mime.application import MIMEApplication
|
||||||
|
|
@ -12,14 +13,67 @@ from config.config import settings
|
||||||
import logging
|
import logging
|
||||||
from reportlab.lib.pagesizes import A4
|
from reportlab.lib.pagesizes import A4
|
||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
|
from reportlab.pdfbase import pdfmetrics
|
||||||
|
from reportlab.pdfbase.ttfonts import TTFont
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from reportlab.lib.units import mm
|
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__)
|
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:
|
class EmailQueue:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.queue = queue.Queue()
|
self.queue = queue.Queue()
|
||||||
|
|
@ -205,252 +259,604 @@ class EmailQueue:
|
||||||
if not doc:
|
if not doc:
|
||||||
raise Exception(f"Document {doc_id} introuvable")
|
raise Exception(f"Document {doc_id} introuvable")
|
||||||
|
|
||||||
buffer = BytesIO()
|
# Générer le PDF avec le nouveau générateur
|
||||||
pdf = canvas.Canvas(buffer, pagesize=A4)
|
generator = SagePDFGenerator(doc, type_doc)
|
||||||
width, height = A4
|
return generator.generate()
|
||||||
|
|
||||||
# Couleurs
|
|
||||||
green_color = HexColor("#2A6F4F")
|
|
||||||
gray_400 = HexColor("#9CA3AF")
|
|
||||||
gray_600 = HexColor("#4B5563")
|
|
||||||
gray_800 = HexColor("#1F2937")
|
|
||||||
|
|
||||||
# Marges
|
class SagePDFGenerator:
|
||||||
margin = 8 * mm
|
# Couleurs Sage
|
||||||
content_width = width - 2 * margin
|
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 =====
|
# Labels des types de documents
|
||||||
y -= 20 * mm
|
TYPE_LABELS = {
|
||||||
|
0: "Devis",
|
||||||
|
10: "Commande",
|
||||||
|
30: "Bon de Livraison",
|
||||||
|
50: "Avoir",
|
||||||
|
60: "Facture",
|
||||||
|
}
|
||||||
|
|
||||||
# Logo/Nom entreprise à gauche
|
def __init__(self, doc_data: dict, type_doc: int):
|
||||||
pdf.setFont("Helvetica-Bold", 18)
|
self.doc = doc_data
|
||||||
pdf.setFillColor(green_color)
|
self.type_doc = type_doc
|
||||||
pdf.drawString(margin, y, "Bijou S.A.S")
|
self.type_label = self.TYPE_LABELS.get(type_doc, "Document")
|
||||||
|
|
||||||
# Informations document à droite
|
# Configuration de la page
|
||||||
pdf.setFillColor(gray_800)
|
self.page_width, self.page_height = A4
|
||||||
pdf.setFont("Helvetica-Bold", 20)
|
self.margin = 15 * mm
|
||||||
numero = doc.get("numero") or "BROUILLON"
|
self.content_width = self.page_width - 2 * self.margin
|
||||||
pdf.drawRightString(width - margin, y, numero.upper())
|
|
||||||
|
|
||||||
y -= 7 * mm
|
# État du générateur
|
||||||
pdf.setFont("Helvetica", 9)
|
self.buffer = BytesIO()
|
||||||
pdf.setFillColor(gray_600)
|
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")
|
# Initialiser les polices
|
||||||
pdf.drawRightString(width - margin, y, f"Date : {date_str}")
|
_register_sage_font()
|
||||||
|
self.use_sage_font = _sage_font_registered
|
||||||
|
|
||||||
y -= 5 * mm
|
def _get_font(self, bold: bool = False) -> str:
|
||||||
date_livraison = (
|
"""Retourne le nom de la police à utiliser."""
|
||||||
doc.get("date_livraison") or doc.get("date_echeance") or date_str
|
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
|
except Exception as e:
|
||||||
reference = doc.get("reference") or "—"
|
logger.warning(f"Impossible de charger le logo: {e}")
|
||||||
pdf.drawRightString(width - margin, y, f"Réf : {reference}")
|
|
||||||
|
|
||||||
# ===== ADDRESSES =====
|
self._draw_text(
|
||||||
y -= 20 * mm
|
x, y - 8 * mm, "Sage", font_size=24, bold=True, color=self.SAGE_GREEN
|
||||||
|
)
|
||||||
|
return 10 * mm
|
||||||
|
|
||||||
# Émetteur (gauche)
|
def _new_page(self):
|
||||||
col1_x = margin
|
"""Crée une nouvelle page."""
|
||||||
col2_x = margin + content_width / 2 + 6 * mm
|
if self.pdf:
|
||||||
col_width = content_width / 2 - 6 * mm
|
self.pdf.showPage()
|
||||||
|
self.page_number += 1
|
||||||
|
self.current_y = self.page_height - self.margin
|
||||||
|
|
||||||
pdf.setFont("Helvetica-Bold", 8)
|
def _draw_header(self):
|
||||||
pdf.setFillColor(gray_400)
|
y = self.page_height - self.margin
|
||||||
pdf.drawString(col1_x, y, "ÉMETTEUR")
|
|
||||||
|
|
||||||
y_emetteur = y - 5 * mm
|
logo_height = self._draw_logo(
|
||||||
pdf.setFont("Helvetica-Bold", 10)
|
self.margin, y, max_width=55 * mm, max_height=22 * mm
|
||||||
pdf.setFillColor(gray_800)
|
)
|
||||||
pdf.drawString(col1_x, y_emetteur, "Bijou S.A.S")
|
|
||||||
|
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
|
y_emetteur -= 5 * mm
|
||||||
pdf.setFont("Helvetica", 9)
|
self._draw_text(
|
||||||
pdf.setFillColor(gray_600)
|
col1_x, y_emetteur, self.COMPANY_ADDRESS_1, font_size=9, color=self.GRAY_600
|
||||||
pdf.drawString(col1_x, y_emetteur, "123 Avenue de la République")
|
)
|
||||||
|
|
||||||
y_emetteur -= 4 * mm
|
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
|
y_emetteur -= 5 * mm
|
||||||
pdf.drawString(col1_x, y_emetteur, "contact@bijou.com")
|
self._draw_text(
|
||||||
|
col1_x, y_emetteur, self.COMPANY_EMAIL, font_size=9, color=self.GRAY_600
|
||||||
# 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)
|
box_padding = 4 * mm
|
||||||
pdf.setFont("Helvetica-Bold", 8)
|
box_height = 26 * mm
|
||||||
pdf.drawString(col2_x + 4 * mm, y, "DESTINATAIRE")
|
box_y = y - 3 * mm
|
||||||
|
|
||||||
y_dest = y - 5 * mm
|
# Fond gris arrondi
|
||||||
pdf.setFont("Helvetica-Bold", 10)
|
self.pdf.setFillColor(self.GRAY_50)
|
||||||
pdf.setFillColor(gray_800)
|
self.pdf.roundRect(
|
||||||
client_name = doc.get("client_intitule") or "Client"
|
col2_x - box_padding,
|
||||||
pdf.drawString(col2_x + 4 * mm, y_dest, client_name)
|
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
|
y_dest -= 5 * mm
|
||||||
pdf.setFont("Helvetica", 9)
|
client_adresse = (
|
||||||
pdf.setFillColor(gray_600)
|
self.doc.get("client_adresse") or self.doc.get("adresse_livraison") or ""
|
||||||
pdf.drawString(col2_x + 4 * mm, y_dest, "10 rue des Clients")
|
)
|
||||||
|
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
|
y_dest -= 4 * mm
|
||||||
pdf.drawString(col2_x + 4 * mm, y_dest, "75001 Paris")
|
|
||||||
|
|
||||||
# ===== LIGNES D'ARTICLES =====
|
client_cp = self.doc.get("client_cp") or ""
|
||||||
y = min(y_emetteur, y_dest) - 20 * mm
|
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
|
return min(y_emetteur, box_y - box_height) - 15 * mm
|
||||||
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)
|
def _draw_table_header(self, y: float) -> float:
|
||||||
pdf.setFillColor(gray_800)
|
col_des = self.margin
|
||||||
pdf.drawString(col_designation, y, "Désignation")
|
col_qte = self.margin + 95 * mm
|
||||||
pdf.drawRightString(col_quantite, y, "Qté")
|
col_pu = self.margin + 115 * mm
|
||||||
pdf.drawRightString(col_prix_unit, y, "Prix Unit. HT")
|
col_rem = self.margin + 140 * mm
|
||||||
pdf.drawRightString(col_taux_taxe, y, "TVA")
|
col_tva = self.margin + 157 * mm
|
||||||
pdf.drawRightString(col_montant, y, "Montant HT")
|
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
|
self._draw_text(
|
||||||
pdf.setFont("Helvetica", 8)
|
col_des, y, "Désignation", font_size=9, bold=True, color=self.GRAY_700
|
||||||
lignes = doc.get("lignes", [])
|
)
|
||||||
|
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:
|
return y - 8 * mm
|
||||||
if y < 60 * mm: # Nouvelle page si nécessaire
|
|
||||||
pdf.showPage()
|
def _draw_line_item(self, y: float, ligne: dict, alternate: bool = False) -> float:
|
||||||
y = height - margin - 20 * mm
|
col_des = self.margin
|
||||||
pdf.setFont("Helvetica", 8)
|
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 = (
|
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", "")
|
description = ligne.get("description", "")
|
||||||
if description and description != designation:
|
line_height = 6 * mm
|
||||||
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
|
if description and description != designation:
|
||||||
y += 4 * mm # Remonter pour aligner avec la désignation
|
y -= 4 * mm
|
||||||
pdf.setFont("Helvetica", 8)
|
if len(description) > 60:
|
||||||
pdf.setFillColor(gray_800)
|
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
|
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)
|
prix_unit = ligne.get("prix_unitaire_ht") or ligne.get("prix_unitaire") or 0
|
||||||
pdf.drawRightString(col_prix_unit, y, f"{prix_unit:.2f} €")
|
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
|
remise = ligne.get("remise_pourcentage") or ligne.get("remise") or 0
|
||||||
pdf.drawRightString(col_taux_taxe, y, f"{taux_taxe}%")
|
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)
|
# TVA
|
||||||
pdf.setFont("Helvetica-Bold", 8)
|
taux_tva = ligne.get("taux_taxe1") or ligne.get("taux_tva") or 20
|
||||||
pdf.drawRightString(col_montant, y, f"{montant:.2f} €")
|
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
|
return y - line_height - 2 * mm
|
||||||
if not lignes:
|
|
||||||
pdf.setFont("Helvetica-Oblique", 9)
|
|
||||||
pdf.setFillColor(gray_400)
|
|
||||||
pdf.drawCentredString(width / 2, y, "Aucune ligne")
|
|
||||||
y -= 15 * mm
|
|
||||||
|
|
||||||
# ===== TOTAUX =====
|
def _draw_totals(self, y: float) -> float:
|
||||||
y -= 10 * mm
|
totals_x = self.page_width - self.margin - 60 * mm
|
||||||
|
right_x = self.page_width - self.margin
|
||||||
|
|
||||||
totals_x = width - margin - 64 * mm
|
self.pdf.setStrokeColor(self.GRAY_200)
|
||||||
totals_label_width = 40 * mm
|
self.pdf.setLineWidth(0.5)
|
||||||
|
self.pdf.line(totals_x - 5 * mm, y + 5 * mm, right_x, y + 5 * mm)
|
||||||
|
|
||||||
pdf.setFont("Helvetica", 9)
|
self._draw_text(totals_x, y, "Total HT", font_size=9, color=self.GRAY_600)
|
||||||
pdf.setFillColor(gray_600)
|
total_ht = self.doc.get("total_ht_net") or self.doc.get("total_ht") or 0
|
||||||
|
self._draw_text(
|
||||||
# Total HT
|
right_x,
|
||||||
pdf.drawString(totals_x, y, "Total HT")
|
y,
|
||||||
total_ht = doc.get("total_ht_net") or doc.get("total_ht") or 0
|
f"{total_ht:.2f} €",
|
||||||
pdf.drawRightString(width - margin, y, f"{total_ht:.2f} €")
|
font_size=9,
|
||||||
|
color=self.GRAY_800,
|
||||||
|
align="right",
|
||||||
|
)
|
||||||
|
|
||||||
y -= 6 * mm
|
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
|
# TVA
|
||||||
pdf.drawString(totals_x, y, "TVA")
|
total_ttc = self.doc.get("total_ttc") or 0
|
||||||
total_ttc = doc.get("total_ttc") or 0
|
tva = total_ttc - total_ht + remise_globale
|
||||||
tva = total_ttc - total_ht
|
taux_tva_global = self.doc.get("taux_tva_principal") or 20
|
||||||
pdf.drawRightString(width - margin, y, f"{tva:.2f} €")
|
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
|
self.pdf.setStrokeColor(self.GRAY_300)
|
||||||
pdf.setStrokeColor(gray_400)
|
self.pdf.setLineWidth(1)
|
||||||
pdf.line(totals_x, y + 2 * mm, width - margin, y + 2 * mm)
|
self.pdf.line(totals_x - 5 * mm, y + 4 * mm, right_x, y + 4 * mm)
|
||||||
|
|
||||||
# Net à payer
|
self._draw_text(
|
||||||
pdf.setFont("Helvetica-Bold", 12)
|
totals_x, y, "Net à payer", font_size=12, bold=True, color=self.SAGE_GREEN
|
||||||
pdf.setFillColor(green_color)
|
)
|
||||||
pdf.drawString(totals_x, y, "Net à payer")
|
self._draw_text(
|
||||||
pdf.drawRightString(width - margin, y, f"{total_ttc:.2f} €")
|
right_x,
|
||||||
|
y,
|
||||||
|
f"{total_ttc:.2f} €",
|
||||||
|
font_size=12,
|
||||||
|
bold=True,
|
||||||
|
color=self.SAGE_GREEN,
|
||||||
|
align="right",
|
||||||
|
)
|
||||||
|
|
||||||
# ===== NOTES =====
|
return y - 15 * mm
|
||||||
notes = doc.get("notes_publique") or doc.get("notes")
|
|
||||||
if notes:
|
def _draw_notes(self, y: float) -> float:
|
||||||
y -= 15 * mm
|
notes = (
|
||||||
pdf.setStrokeColor(gray_400)
|
self.doc.get("notes_publique")
|
||||||
pdf.line(margin, y + 5 * mm, width - margin, y + 5 * mm)
|
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
|
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"):
|
for line in notes.split("\n"):
|
||||||
if y < 25 * mm:
|
if y < 30 * mm:
|
||||||
break
|
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
|
y -= 4 * mm
|
||||||
|
|
||||||
# ===== FOOTER =====
|
return y
|
||||||
pdf.setFont("Helvetica", 7)
|
|
||||||
pdf.setFillColor(gray_400)
|
|
||||||
pdf.drawCentredString(width / 2, 15 * mm, "Page 1 / 1")
|
|
||||||
|
|
||||||
pdf.save()
|
def _draw_footer(self):
|
||||||
buffer.seek(0)
|
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()
|
email_queue = EmailQueue()
|
||||||
|
|
|
||||||
BIN
sage/pdfs/Sage_Text-Bold.ttf
Normal file
BIN
sage/pdfs/Sage_Text-Bold.ttf
Normal file
Binary file not shown.
BIN
sage/pdfs/Sage_Text-Medium.ttf
Normal file
BIN
sage/pdfs/Sage_Text-Medium.ttf
Normal file
Binary file not shown.
BIN
sage/pdfs/logo.png
Normal file
BIN
sage/pdfs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
|
|
@ -36,7 +36,7 @@ class UniversignDocumentService:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
documents = data.get("documents", [])
|
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
|
# Log détaillé de chaque document
|
||||||
for idx, doc in enumerate(documents):
|
for idx, doc in enumerate(documents):
|
||||||
|
|
@ -90,7 +90,7 @@ class UniversignDocumentService:
|
||||||
content_length = response.headers.get("Content-Length", "unknown")
|
content_length = response.headers.get("Content-Length", "unknown")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"✅ Téléchargement réussi: "
|
f"Téléchargement réussi: "
|
||||||
f"Content-Type={content_type}, Size={content_length}"
|
f"Content-Type={content_type}, Size={content_length}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -100,7 +100,7 @@ class UniversignDocumentService:
|
||||||
and "octet-stream" not in content_type.lower()
|
and "octet-stream" not in content_type.lower()
|
||||||
):
|
):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"⚠️ Type de contenu inattendu: {content_type}. "
|
f"Type de contenu inattendu: {content_type}. "
|
||||||
f"Tentative de lecture quand même..."
|
f"Tentative de lecture quand même..."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -146,7 +146,7 @@ class UniversignDocumentService:
|
||||||
if not force and transaction.signed_document_path:
|
if not force and transaction.signed_document_path:
|
||||||
if os.path.exists(transaction.signed_document_path):
|
if os.path.exists(transaction.signed_document_path):
|
||||||
logger.debug(
|
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
|
return True, None
|
||||||
|
|
||||||
|
|
@ -162,7 +162,7 @@ class UniversignDocumentService:
|
||||||
|
|
||||||
if not documents:
|
if not documents:
|
||||||
error = "Aucun document trouvé dans la transaction Universign"
|
error = "Aucun document trouvé dans la transaction Universign"
|
||||||
logger.warning(f"⚠️ {error}")
|
logger.warning(f"{error}")
|
||||||
transaction.download_error = error
|
transaction.download_error = error
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return False, error
|
return False, error
|
||||||
|
|
@ -232,7 +232,7 @@ class UniversignDocumentService:
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
logger.info(
|
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
|
return True, None
|
||||||
|
|
|
||||||
|
|
@ -372,7 +372,7 @@ class UniversignSyncService:
|
||||||
# Vérifier la transition
|
# Vérifier la transition
|
||||||
if not is_transition_allowed(previous_local_status, new_local_status):
|
if not is_transition_allowed(previous_local_status, new_local_status):
|
||||||
logger.warning(
|
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(
|
new_local_status = resolve_status_conflict(
|
||||||
previous_local_status, new_local_status
|
previous_local_status, new_local_status
|
||||||
|
|
@ -392,7 +392,7 @@ class UniversignSyncService:
|
||||||
universign_status_raw
|
universign_status_raw
|
||||||
)
|
)
|
||||||
except ValueError:
|
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":
|
if new_local_status == "SIGNE":
|
||||||
transaction.universign_status = (
|
transaction.universign_status = (
|
||||||
UniversignTransactionStatus.COMPLETED
|
UniversignTransactionStatus.COMPLETED
|
||||||
|
|
@ -415,7 +415,7 @@ class UniversignSyncService:
|
||||||
|
|
||||||
if new_local_status == "SIGNE" and not transaction.signed_at:
|
if new_local_status == "SIGNE" and not transaction.signed_at:
|
||||||
transaction.signed_at = datetime.now()
|
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:
|
if new_local_status == "REFUSE" and not transaction.refused_at:
|
||||||
transaction.refused_at = datetime.now()
|
transaction.refused_at = datetime.now()
|
||||||
|
|
@ -450,9 +450,9 @@ class UniversignSyncService:
|
||||||
)
|
)
|
||||||
|
|
||||||
if download_success:
|
if download_success:
|
||||||
logger.info("✅ Document signé téléchargé et stocké")
|
logger.info("Document signé téléchargé et stocké")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ Échec téléchargement: {download_error}")
|
logger.warning(f"Échec téléchargement: {download_error}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
@ -499,7 +499,7 @@ class UniversignSyncService:
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"✅ Sync terminée: {transaction.transaction_id} | "
|
f"Sync terminée: {transaction.transaction_id} | "
|
||||||
f"{previous_local_status} → {new_local_status}"
|
f"{previous_local_status} → {new_local_status}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -557,9 +557,9 @@ class UniversignSyncService:
|
||||||
)
|
)
|
||||||
|
|
||||||
if download_success:
|
if download_success:
|
||||||
logger.info("✅ Document signé téléchargé avec succès")
|
logger.info("Document signé téléchargé avec succès")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"⚠️ Échec téléchargement: {download_error}")
|
logger.warning(f"Échec téléchargement: {download_error}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue