feat(pdf): add dynamic company info to PDF generator
This commit is contained in:
parent
08665f15dd
commit
3f1dce918d
1 changed files with 159 additions and 22 deletions
181
email_queue.py
181
email_queue.py
|
|
@ -20,7 +20,6 @@ from io import BytesIO
|
||||||
from reportlab.lib.units import mm
|
from reportlab.lib.units import mm
|
||||||
from reportlab.lib.colors import HexColor, Color
|
from reportlab.lib.colors import HexColor, Color
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from sqlalchemy.exc import OperationalError
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -82,6 +81,8 @@ async def execute_with_retry(
|
||||||
base_delay: float = 0.1,
|
base_delay: float = 0.1,
|
||||||
max_delay: float = 2.0,
|
max_delay: float = 2.0,
|
||||||
):
|
):
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
|
|
||||||
last_exception = None
|
last_exception = None
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
|
|
@ -106,7 +107,7 @@ async def execute_with_retry(
|
||||||
else:
|
else:
|
||||||
# Autre erreur OperationalError, ne pas retry
|
# Autre erreur OperationalError, ne pas retry
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# Autres exceptions, ne pas retry
|
# Autres exceptions, ne pas retry
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
@ -372,8 +373,18 @@ class EmailQueue:
|
||||||
if not doc:
|
if not doc:
|
||||||
raise Exception(f"Document {doc_id} introuvable")
|
raise Exception(f"Document {doc_id} introuvable")
|
||||||
|
|
||||||
|
# Récupérer les informations de la société émettrice
|
||||||
|
societe_info = None
|
||||||
|
try:
|
||||||
|
societe_info = self.sage_client.lire_informations_societe()
|
||||||
|
logger.debug(
|
||||||
|
f"Infos société récupérées: {societe_info.raison_sociale if societe_info else 'N/A'}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Impossible de récupérer les infos société: {e}")
|
||||||
|
|
||||||
# Générer le PDF avec le nouveau générateur
|
# Générer le PDF avec le nouveau générateur
|
||||||
generator = SagePDFGenerator(doc, type_doc)
|
generator = SagePDFGenerator(doc, type_doc, societe_info=societe_info)
|
||||||
return generator.generate()
|
return generator.generate()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -392,10 +403,11 @@ class SagePDFGenerator:
|
||||||
GRAY_800 = HexColor("#1F2937")
|
GRAY_800 = HexColor("#1F2937")
|
||||||
GRAY_900 = HexColor("#111827")
|
GRAY_900 = HexColor("#111827")
|
||||||
|
|
||||||
COMPANY_NAME = "Bijou S.A.S"
|
# Valeurs par défaut (fallback si société non disponible)
|
||||||
COMPANY_ADDRESS_1 = "123 Avenue de la République"
|
DEFAULT_COMPANY_NAME = "Entreprise"
|
||||||
COMPANY_ADDRESS_2 = "75011 Paris, France"
|
DEFAULT_COMPANY_ADDRESS = ""
|
||||||
COMPANY_EMAIL = "contact@bijou.com"
|
DEFAULT_COMPANY_CITY = ""
|
||||||
|
DEFAULT_COMPANY_EMAIL = ""
|
||||||
|
|
||||||
# Labels des types de documents
|
# Labels des types de documents
|
||||||
TYPE_LABELS = {
|
TYPE_LABELS = {
|
||||||
|
|
@ -406,10 +418,11 @@ class SagePDFGenerator:
|
||||||
60: "Facture",
|
60: "Facture",
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, doc_data: dict, type_doc: int):
|
def __init__(self, doc_data: dict, type_doc: int, societe_info=None):
|
||||||
self.doc = doc_data
|
self.doc = doc_data
|
||||||
self.type_doc = type_doc
|
self.type_doc = type_doc
|
||||||
self.type_label = self.TYPE_LABELS.get(type_doc, "Document")
|
self.type_label = self.TYPE_LABELS.get(type_doc, "Document")
|
||||||
|
self.societe_info = societe_info
|
||||||
|
|
||||||
# Configuration de la page
|
# Configuration de la page
|
||||||
self.page_width, self.page_height = A4
|
self.page_width, self.page_height = A4
|
||||||
|
|
@ -427,6 +440,64 @@ class SagePDFGenerator:
|
||||||
_register_sage_font()
|
_register_sage_font()
|
||||||
self.use_sage_font = _sage_font_registered
|
self.use_sage_font = _sage_font_registered
|
||||||
|
|
||||||
|
def _get_societe_field(self, field: str, default: str = "") -> str:
|
||||||
|
"""Récupère un champ de la société avec fallback."""
|
||||||
|
if self.societe_info is None:
|
||||||
|
return default
|
||||||
|
return getattr(self.societe_info, field, default) or default
|
||||||
|
|
||||||
|
def _get_societe_name(self) -> str:
|
||||||
|
"""Retourne la raison sociale de la société."""
|
||||||
|
return self._get_societe_field("raison_sociale", self.DEFAULT_COMPANY_NAME)
|
||||||
|
|
||||||
|
def _get_societe_address_line1(self) -> str:
|
||||||
|
"""Retourne la première ligne d'adresse."""
|
||||||
|
adresse = self._get_societe_field("adresse", "")
|
||||||
|
complement = self._get_societe_field("complement_adresse", "")
|
||||||
|
|
||||||
|
if adresse and complement:
|
||||||
|
return f"{adresse}, {complement}"
|
||||||
|
return adresse or complement or self.DEFAULT_COMPANY_ADDRESS
|
||||||
|
|
||||||
|
def _get_societe_address_line2(self) -> str:
|
||||||
|
"""Retourne la deuxième ligne d'adresse (CP + Ville + Pays)."""
|
||||||
|
cp = self._get_societe_field("code_postal", "")
|
||||||
|
ville = self._get_societe_field("ville", "")
|
||||||
|
pays = self._get_societe_field("pays", "")
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if cp:
|
||||||
|
parts.append(cp)
|
||||||
|
if ville:
|
||||||
|
parts.append(ville)
|
||||||
|
|
||||||
|
city_line = " ".join(parts)
|
||||||
|
|
||||||
|
if pays and pays.lower() not in ["france", "fr"]:
|
||||||
|
city_line = f"{city_line}, {pays}" if city_line else pays
|
||||||
|
|
||||||
|
return city_line or self.DEFAULT_COMPANY_CITY
|
||||||
|
|
||||||
|
def _get_societe_email(self) -> str:
|
||||||
|
"""Retourne l'email de la société."""
|
||||||
|
# Priorité à email_societe, puis email
|
||||||
|
email = self._get_societe_field("email_societe", "")
|
||||||
|
if not email:
|
||||||
|
email = self._get_societe_field("email", "")
|
||||||
|
return email or self.DEFAULT_COMPANY_EMAIL
|
||||||
|
|
||||||
|
def _get_societe_phone(self) -> str:
|
||||||
|
"""Retourne le téléphone de la société."""
|
||||||
|
return self._get_societe_field("telephone", "")
|
||||||
|
|
||||||
|
def _get_societe_siret(self) -> str:
|
||||||
|
"""Retourne le SIRET de la société."""
|
||||||
|
return self._get_societe_field("siret", "")
|
||||||
|
|
||||||
|
def _get_societe_tva(self) -> str:
|
||||||
|
"""Retourne le numéro de TVA de la société."""
|
||||||
|
return self._get_societe_field("numero_tva", "")
|
||||||
|
|
||||||
def _get_font(self, bold: bool = False) -> str:
|
def _get_font(self, bold: bool = False) -> str:
|
||||||
"""Retourne le nom de la police à utiliser."""
|
"""Retourne le nom de la police à utiliser."""
|
||||||
if self.use_sage_font:
|
if self.use_sage_font:
|
||||||
|
|
@ -563,31 +634,57 @@ class SagePDFGenerator:
|
||||||
col1_x, y, "ÉMETTEUR", font_size=8, bold=True, color=self.GRAY_400
|
col1_x, y, "ÉMETTEUR", font_size=8, bold=True, color=self.GRAY_400
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# === INFORMATIONS SOCIÉTÉ (dynamiques) ===
|
||||||
y_emetteur = y - 6 * mm
|
y_emetteur = y - 6 * mm
|
||||||
self._draw_text(
|
self._draw_text(
|
||||||
col1_x,
|
col1_x,
|
||||||
y_emetteur,
|
y_emetteur,
|
||||||
self.COMPANY_NAME,
|
self._get_societe_name(),
|
||||||
font_size=10,
|
font_size=10,
|
||||||
bold=True,
|
bold=True,
|
||||||
color=self.GRAY_800,
|
color=self.GRAY_800,
|
||||||
)
|
)
|
||||||
|
|
||||||
y_emetteur -= 5 * mm
|
# Adresse ligne 1
|
||||||
self._draw_text(
|
address_line1 = self._get_societe_address_line1()
|
||||||
col1_x, y_emetteur, self.COMPANY_ADDRESS_1, font_size=9, color=self.GRAY_600
|
if address_line1:
|
||||||
)
|
y_emetteur -= 5 * mm
|
||||||
|
# Tronquer si trop long
|
||||||
|
if len(address_line1) > 45:
|
||||||
|
address_line1 = address_line1[:42] + "..."
|
||||||
|
self._draw_text(
|
||||||
|
col1_x, y_emetteur, address_line1, font_size=9, color=self.GRAY_600
|
||||||
|
)
|
||||||
|
|
||||||
y_emetteur -= 4 * mm
|
# Adresse ligne 2 (CP + Ville)
|
||||||
self._draw_text(
|
address_line2 = self._get_societe_address_line2()
|
||||||
col1_x, y_emetteur, self.COMPANY_ADDRESS_2, font_size=9, color=self.GRAY_600
|
if address_line2:
|
||||||
)
|
y_emetteur -= 4 * mm
|
||||||
|
self._draw_text(
|
||||||
|
col1_x, y_emetteur, address_line2, font_size=9, color=self.GRAY_600
|
||||||
|
)
|
||||||
|
|
||||||
y_emetteur -= 5 * mm
|
# Email
|
||||||
self._draw_text(
|
societe_email = self._get_societe_email()
|
||||||
col1_x, y_emetteur, self.COMPANY_EMAIL, font_size=9, color=self.GRAY_600
|
if societe_email:
|
||||||
)
|
y_emetteur -= 5 * mm
|
||||||
|
self._draw_text(
|
||||||
|
col1_x, y_emetteur, societe_email, font_size=9, color=self.GRAY_600
|
||||||
|
)
|
||||||
|
|
||||||
|
# Téléphone (optionnel)
|
||||||
|
societe_phone = self._get_societe_phone()
|
||||||
|
if societe_phone:
|
||||||
|
y_emetteur -= 4 * mm
|
||||||
|
self._draw_text(
|
||||||
|
col1_x,
|
||||||
|
y_emetteur,
|
||||||
|
f"Tél: {societe_phone}",
|
||||||
|
font_size=8,
|
||||||
|
color=self.GRAY_500,
|
||||||
|
)
|
||||||
|
|
||||||
|
# === DESTINATAIRE ===
|
||||||
box_padding = 4 * mm
|
box_padding = 4 * mm
|
||||||
box_height = 26 * mm
|
box_height = 26 * mm
|
||||||
box_y = y - 3 * mm
|
box_y = y - 3 * mm
|
||||||
|
|
@ -912,8 +1009,48 @@ class SagePDFGenerator:
|
||||||
return y
|
return y
|
||||||
|
|
||||||
def _draw_footer(self):
|
def _draw_footer(self):
|
||||||
footer_y = 12 * mm
|
footer_y = 15 * mm
|
||||||
|
|
||||||
|
# Informations légales de la société
|
||||||
|
legal_parts = []
|
||||||
|
|
||||||
|
societe_name = self._get_societe_name()
|
||||||
|
if societe_name and societe_name != self.DEFAULT_COMPANY_NAME:
|
||||||
|
legal_parts.append(societe_name)
|
||||||
|
|
||||||
|
siret = self._get_societe_siret()
|
||||||
|
if siret:
|
||||||
|
legal_parts.append(f"SIRET: {siret}")
|
||||||
|
|
||||||
|
tva = self._get_societe_tva()
|
||||||
|
if tva:
|
||||||
|
legal_parts.append(f"TVA: {tva}")
|
||||||
|
|
||||||
|
# Forme juridique et capital si disponibles
|
||||||
|
if self.societe_info:
|
||||||
|
forme = self._get_societe_field("forme_juridique", "")
|
||||||
|
capital = getattr(self.societe_info, "capital", 0)
|
||||||
|
if forme and capital > 0:
|
||||||
|
legal_parts.append(
|
||||||
|
f"{forme} au capital de {capital:,.0f} €".replace(",", " ")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dessiner les informations légales
|
||||||
|
if legal_parts:
|
||||||
|
legal_text = " • ".join(legal_parts)
|
||||||
|
# Tronquer si trop long
|
||||||
|
if len(legal_text) > 100:
|
||||||
|
legal_text = legal_text[:97] + "..."
|
||||||
|
self._draw_text(
|
||||||
|
self.page_width / 2,
|
||||||
|
footer_y + 4 * mm,
|
||||||
|
legal_text,
|
||||||
|
font_size=7,
|
||||||
|
color=self.GRAY_400,
|
||||||
|
align="center",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Pagination
|
||||||
page_text = f"Page {self.page_number} / {self.total_pages}"
|
page_text = f"Page {self.page_number} / {self.total_pages}"
|
||||||
self._draw_text(
|
self._draw_text(
|
||||||
self.page_width / 2,
|
self.page_width / 2,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue