import os from io import BytesIO from PIL import Image, ImageDraw, ImageFont class BankStatementGenerator: WIDTH = 900 ROW_HEIGHT = 38 HEADER_HEIGHT = 180 FOOTER_HEIGHT = 80 def __init__(self): self.font_title = ImageFont.load_default() self.font_header = ImageFont.load_default() self.font_body = ImageFont.load_default() self.font_body_bold = ImageFont.load_default() self.font_small = ImageFont.load_default() self.font_amount_pos = ImageFont.load_default() self.font_amount_neg = ImageFont.load_default() try: base = os.path.join(os.path.dirname(__file__), "..", "ressources", "fonts") if os.path.isdir(base): for name in os.listdir(base): path = os.path.join(base, name) lower = name.lower() if "bold" in lower: self.font_title = ImageFont.truetype(path, 30) self.font_header = ImageFont.truetype(path, 16) self.font_body_bold = ImageFont.truetype(path, 15) self.font_amount_pos = ImageFont.truetype(path, 15) self.font_amount_neg = ImageFont.truetype(path, 15) elif "mono" in lower or "regular" in lower or "light" in lower: self.font_body = ImageFont.truetype(path, 14) self.font_small = ImageFont.truetype(path, 12) except Exception: pass def _rounded_rect(self, draw, xy, radius, fill=None, outline=None, width=0): x0, y0, x1, y1 = xy r = min(radius, (x1 - x0) // 2, (y1 - y0) // 2) if fill: draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill) draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill) draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill) draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill) draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill) draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill) if outline and width: draw.line([x0 + r, y0, x1 - r, y0], fill=outline, width=width) draw.line([x0 + r, y1, x1 - r, y1], fill=outline, width=width) draw.line([x0, y0 + r, x0, y1 - r], fill=outline, width=width) draw.line([x1, y0 + r, x1, y1 - r], fill=outline, width=width) def generate( self, bank_name: str, holder_name: str, rib: str, solde_actuel: float, transactions: list, account_type: str = "particulier", ) -> BytesIO: bg = (15, 15, 22) card_bg = (22, 24, 35) accent = (0, 190, 170) gold = (212, 175, 55) white = (240, 240, 245) gray = (140, 145, 160) dim = (70, 75, 90) row_even = (26, 28, 40) row_odd = (22, 24, 35) green = (40, 200, 120) red = (220, 70, 70) line_color = (40, 42, 55) n_rows = len(transactions) total_h = self.HEADER_HEIGHT + n_rows * self.ROW_HEIGHT + self.FOOTER_HEIGHT + 60 img = Image.new("RGB", (self.WIDTH, total_h), bg) draw = ImageDraw.Draw(img) self._rounded_rect(draw, (20, 20, self.WIDTH - 20, 50), 10, fill=card_bg) draw.text((35, 25), "RELEVE BANCAIRE", fill=accent, font=self.font_title) y = 80 draw.line([(30, y), (self.WIDTH - 30, y)], fill=line_color, width=1) y += 15 col_labels = [ (35, "DATE"), (160, "TYPE"), (380, "EMETTEUR / DESTINATAIRE"), (620, "MONTANT"), ] for cx, label in col_labels: draw.text((cx, y), label, fill=gray, font=self.font_header) y += 25 draw.text((35, y), f"Banque : {bank_name}", fill=white, font=self.font_body_bold) draw.text((35, y + 20), f"Titulaire : {holder_name}", fill=white, font=self.font_body) draw.text((35, y + 40), f"RIB : {rib}", fill=gray, font=self.font_small) solde_label = f"Solde actuel : {solde_actuel:,.2f} EUR".replace(",", " ") draw.text((self.WIDTH - 300, y + 10), solde_label, fill=gold, font=self.font_body_bold) type_label = "Entreprise" if account_type == "entreprise" else "Particulier" draw.text((self.WIDTH - 300, y + 32), f"Type : {type_label}", fill=gray, font=self.font_small) y = self.HEADER_HEIGHT + 20 draw.line([(30, y - 5), (self.WIDTH - 30, y - 5)], fill=line_color, width=1) for i, tx in enumerate(transactions): ry = y + i * self.ROW_HEIGHT row_bg = row_even if i % 2 == 0 else row_odd draw.rectangle([(30, ry), (self.WIDTH - 30, ry + self.ROW_HEIGHT)], fill=row_bg) date_str = tx.get("date", "N/A") if len(date_str) > 16: date_str = date_str[:16] tx_type = tx.get("type", "N/A") type_display = { "virement": "Virement", "depot_banquier": "Depot", "retrait_banquier": "Retrait", "tva_prelevee": "TVA Prelevee", }.get(tx_type, tx_type) emetteur_rib = tx.get("emetteur_rib", "") destinataire_rib = tx.get("destinataire_rib", "") montant_val = tx.get("montant", 0) emetteur_id = tx.get("emetteur_id") destinataire_id = tx.get("destinataire_id") if emetteur_id: other_ref = f"De: <@{emetteur_id}>" elif emetteur_rib: other_ref = f"De: ...{emetteur_rib[-6:]}" if emetteur_rib else "" elif destinataire_id: other_ref = f"A: <@{destinataire_id}>" elif destinataire_rib: other_ref = f"A: ...{destinataire_rib[-6:]}" if destinataire_rib else "" else: other_ref = "" metadata = tx.get("metadata", "") if metadata: try: import json meta = json.loads(metadata) if "banquier_id" in meta: other_ref += f" (banquier: {meta['banquier_id']})" except Exception: pass draw.text((35, ry + 10), date_str, fill=gray, font=self.font_body) draw.text((160, ry + 10), type_display, fill=white, font=self.font_body_bold) draw.text((380, ry + 10), other_ref, fill=gray, font=self.font_body) is_incoming = tx_type in ("depot_banquier",) or ( tx_type == "virement" and destinataire_rib and not emetteur_rib ) if tx_type == "virement": is_incoming = False if tx_type == "tva_prelevee": amt_color = red amt_str = f"-{montant_val:,.2f} EUR".replace(",", " ") elif is_incoming: amt_color = green amt_str = f"+{montant_val:,.2f} EUR".replace(",", " ") else: amt_color = red if montant_val > 0 else gray amt_str = f"-{montant_val:,.2f} EUR".replace(",", " ") draw.text((620, ry + 10), amt_str, fill=amt_color, font=self.font_amount_pos) draw.line([(30, ry + self.ROW_HEIGHT), (self.WIDTH - 30, ry + self.ROW_HEIGHT)], fill=line_color, width=1) footer_y = y + n_rows * self.ROW_HEIGHT + 15 draw.line([(30, footer_y), (self.WIDTH - 30, footer_y)], fill=line_color, width=1) footer_y += 10 draw.text((35, footer_y), f"{n_rows} transaction(s)", fill=gray, font=self.font_small) draw.text((35, footer_y + 18), "Victoria's Vice — Systeme Bancaire V2", fill=dim, font=self.font_small) buf = BytesIO() img.save(buf, format="PNG") buf.seek(0) return buf