Vicy_Publique/utils/bank_statement.py

189 lines
7.7 KiB
Python
Raw Permalink Normal View History

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