- bank_card: premium design (hologram, contactless, security pattern, glow, signature) - bank_statement: new Pillow generator with transaction history table - panel: fix duplicate custom_ids (hash-based), fix ComptePanelView loading - panel: fix BanquierPanelView user select integration - panel: add Releve button on account detail, add consulter-clients/info/plafond/reserve - panel: add user display name in account view, all bank commands accessible from panel
188 lines
7.7 KiB
Python
188 lines
7.7 KiB
Python
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
|