V2.2: Bank card redesign, bank statement, panel bug fixes
- 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
This commit is contained in:
parent
48ec3a3d89
commit
c37a2a0ff9
3 changed files with 802 additions and 603 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +1,13 @@
|
|||
import os
|
||||
import math
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
|
||||
|
||||
class BankCardGenerator:
|
||||
WIDTH = 856
|
||||
HEIGHT = 540
|
||||
RADIUS = 30
|
||||
|
||||
COLORS = {
|
||||
"dark": (25, 25, 35),
|
||||
"mid": (35, 40, 60),
|
||||
"light": (50, 55, 80),
|
||||
"accent": (0, 180, 160),
|
||||
"accent_dim": (0, 120, 105),
|
||||
"gold": (212, 175, 55),
|
||||
"white": (255, 255, 255),
|
||||
"gray": (160, 165, 180),
|
||||
"dim": (90, 95, 110),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.font_title = ImageFont.load_default()
|
||||
|
|
@ -27,6 +15,8 @@ class BankCardGenerator:
|
|||
self.font_value = ImageFont.load_default()
|
||||
self.font_rib = ImageFont.load_default()
|
||||
self.font_large = ImageFont.load_default()
|
||||
self.font_huge = ImageFont.load_default()
|
||||
self.font_small = ImageFont.load_default()
|
||||
try:
|
||||
base = os.path.join(os.path.dirname(__file__), "..", "ressources", "fonts")
|
||||
if os.path.isdir(base):
|
||||
|
|
@ -34,27 +24,30 @@ class BankCardGenerator:
|
|||
path = os.path.join(base, name)
|
||||
lower = name.lower()
|
||||
if "bold" in lower:
|
||||
self.font_title = ImageFont.truetype(path, 26)
|
||||
self.font_large = ImageFont.truetype(path, 36)
|
||||
elif "mono" in lower or "regular" in lower:
|
||||
self.font_label = ImageFont.truetype(path, 16)
|
||||
self.font_title = ImageFont.truetype(path, 28)
|
||||
self.font_large = ImageFont.truetype(path, 38)
|
||||
self.font_huge = ImageFont.truetype(path, 48)
|
||||
elif "mono" in lower or "regular" in lower or "light" in lower:
|
||||
self.font_label = ImageFont.truetype(path, 15)
|
||||
self.font_value = ImageFont.truetype(path, 22)
|
||||
self.font_rib = ImageFont.truetype(path, 20)
|
||||
self.font_rib = ImageFont.truetype(path, 24)
|
||||
self.font_small = ImageFont.truetype(path, 12)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _rounded_rect(self, draw, xy, radius, fill, outline=None, width=0):
|
||||
def _rounded_rect(self, draw, xy, radius, fill=None, outline=None, width=0):
|
||||
x0, y0, x1, y1 = xy
|
||||
r = radius
|
||||
draw.ellipse([x0, y0, x0 + 2 * r, y0 + 2 * r], fill=fill, outline=outline, width=width)
|
||||
draw.ellipse([x1 - 2 * r, y0, x1, y0 + 2 * r], fill=fill, outline=outline, width=width)
|
||||
draw.ellipse([x0, y1 - 2 * r, x0 + 2 * r, y1], fill=fill, outline=outline, width=width)
|
||||
draw.ellipse([x1 - 2 * r, y1 - 2 * r, x1, y1], fill=fill, outline=outline, width=width)
|
||||
draw.rectangle([x0 + r, y0, x1 - r, y1], fill=fill)
|
||||
draw.rectangle([x0, y0 + r, x1, y1 - r], fill=fill)
|
||||
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.arc([x0, y0, x0 + 2 * r, y0 + 2 * r], 180, 270, fill=outline, width=width)
|
||||
draw.arc([x1 - 2 * r, y0, x1, y0 + 2 * r], 270, 0, fill=outline, width=width)
|
||||
draw.arc([x1 - 2 * r, y0, x1, y0 + 2 * r], 270, 360, fill=outline, width=width)
|
||||
draw.arc([x0, y1 - 2 * r, x0 + 2 * r, y1], 90, 180, fill=outline, width=width)
|
||||
draw.arc([x1 - 2 * r, y1 - 2 * r, x1, y1], 0, 90, fill=outline, width=width)
|
||||
draw.line([x0 + r, y0, x1 - r, y0], fill=outline, width=width)
|
||||
|
|
@ -62,40 +55,65 @@ class BankCardGenerator:
|
|||
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 _draw_gradient_rect(self, draw, xy, color_start, color_end, vertical=True):
|
||||
def _gradient(self, draw, xy, c1, c2, vertical=True):
|
||||
x0, y0, x1, y1 = xy
|
||||
if vertical:
|
||||
for y in range(y0, y1):
|
||||
t = (y - y0) / max(1, y1 - y0)
|
||||
r = int(color_start[0] + (color_end[0] - color_start[0]) * t)
|
||||
g = int(color_start[1] + (color_end[1] - color_start[1]) * t)
|
||||
b = int(color_start[2] + (color_end[2] - color_start[2]) * t)
|
||||
draw.line([(x0, y), (x1, y)], fill=(r, g, b))
|
||||
else:
|
||||
for x in range(x0, x1):
|
||||
t = (x - x0) / max(1, x1 - x0)
|
||||
r = int(color_start[0] + (color_end[0] - color_start[0]) * t)
|
||||
g = int(color_start[1] + (color_end[1] - color_start[1]) * t)
|
||||
b = int(color_start[2] + (color_end[2] - color_start[2]) * t)
|
||||
draw.line([(x, y0), (x, y1)], fill=(r, g, b))
|
||||
rng = range(y0, y1) if vertical else range(x0, x1)
|
||||
length = (y1 - y0) if vertical else (x1 - x0)
|
||||
for i in rng:
|
||||
t = (i - (y0 if vertical else x0)) / max(1, length)
|
||||
r = int(c1[0] + (c2[0] - c1[0]) * t)
|
||||
g = int(c1[1] + (c2[1] - c1[1]) * t)
|
||||
b = int(c1[2] + (c2[2] - c1[2]) * t)
|
||||
if vertical:
|
||||
draw.line([(x0, i), (x1, i)], fill=(r, g, b))
|
||||
else:
|
||||
draw.line([(i, y0), (i, y1)], fill=(r, g, b))
|
||||
|
||||
def _draw_chip(self, draw, x, y):
|
||||
w, h = 55, 40
|
||||
self._rounded_rect(draw, (x, y, x + w, y + h), 6, fill=(200, 175, 50), outline=(180, 155, 40), width=2)
|
||||
draw.line([(x + 8, y + h // 3), (x + w - 8, y + h // 3)], fill=(160, 135, 30), width=1)
|
||||
draw.line([(x + 8, y + 2 * h // 3), (x + w - 8, y + 2 * h // 3)], fill=(160, 135, 30), width=1)
|
||||
draw.line([(x + w // 3, y + 4), (x + w // 3, y + h - 4)], fill=(160, 135, 30), width=1)
|
||||
draw.line([(x + 2 * w // 3, y + 4), (x + 2 * w // 3, y + h - 4)], fill=(160, 135, 30), width=1)
|
||||
w, h = 60, 45
|
||||
self._rounded_rect(draw, (x, y, x + w, y + h), 8, fill=(210, 180, 55))
|
||||
self._rounded_rect(draw, (x + 3, y + 3, x + w - 3, y + h - 3), 6, fill=(225, 195, 70))
|
||||
for dy in [h // 3, 2 * h // 3]:
|
||||
draw.line([(x + 8, y + dy), (x + w - 8, y + dy)], fill=(190, 160, 40), width=1)
|
||||
for dx in [w // 3, 2 * w // 3]:
|
||||
draw.line([(x + dx, y + 5), (x + dx, y + h - 5)], fill=(190, 160, 40), width=1)
|
||||
draw.arc([x + w // 2 - 12, y + h // 2 - 12, x + w // 2 + 12, y + h // 2 + 12], 0, 360, fill=(190, 160, 40), width=1)
|
||||
|
||||
def _draw_wave(self, draw, y_base, color, amplitude=15, segments=8):
|
||||
points = []
|
||||
w = self.WIDTH
|
||||
for i in range(segments + 1):
|
||||
x = int(w * i / segments)
|
||||
y = y_base + int(amplitude * math.sin(i * 1.2))
|
||||
points.append((x, y))
|
||||
for i in range(len(points) - 1):
|
||||
draw.line([points[i], points[i + 1]], fill=color, width=2)
|
||||
def _draw_hologram(self, draw, x, y, size=50):
|
||||
for i in range(5):
|
||||
offset = i * 3
|
||||
alpha_color = (
|
||||
100 + i * 30,
|
||||
200 - i * 20,
|
||||
220 - i * 15,
|
||||
)
|
||||
draw.ellipse(
|
||||
[x + offset, y + offset, x + size - offset, y + size - offset],
|
||||
outline=alpha_color, width=1,
|
||||
)
|
||||
draw.ellipse([x + size // 2 - 5, y + size // 2 - 5, x + size // 2 + 5, y + size // 2 + 5], fill=(180, 220, 240))
|
||||
|
||||
def _draw_contactless(self, draw, x, y):
|
||||
for i, r in enumerate([10, 16, 22]):
|
||||
draw.arc([x - r, y - r, x + r, y + r], -60, 60, fill=(180, 180, 190), width=1)
|
||||
|
||||
def _draw_security_pattern(self, draw, width, height):
|
||||
for x in range(0, width, 40):
|
||||
for y in range(0, height, 40):
|
||||
h = hashlib.md5(f"{x}{y}".encode()).hexdigest()
|
||||
if int(h[:2], 16) < 15:
|
||||
draw.point((x, y), fill=(60, 65, 85))
|
||||
|
||||
def _draw_glow(self, img, x, y, radius=80, color=(0, 180, 160), intensity=30):
|
||||
overlay = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||
od = ImageDraw.Draw(overlay)
|
||||
for r in range(radius, 0, -2):
|
||||
alpha = int(intensity * (r / radius))
|
||||
od.ellipse(
|
||||
[x - r, y - r, x + r, y + r],
|
||||
fill=(*color, alpha),
|
||||
)
|
||||
return Image.alpha_composite(img.convert("RGBA"), overlay)
|
||||
|
||||
def generate(
|
||||
self,
|
||||
|
|
@ -106,51 +124,84 @@ class BankCardGenerator:
|
|||
account_type: str = "particulier",
|
||||
tva_rate: float = 0.0,
|
||||
) -> BytesIO:
|
||||
img = Image.new("RGB", (self.WIDTH, self.HEIGHT), self.COLORS["dark"])
|
||||
dark1 = (18, 18, 28)
|
||||
dark2 = (30, 32, 48)
|
||||
accent = (0, 190, 170)
|
||||
accent_dim = (0, 130, 115)
|
||||
gold = (212, 175, 55)
|
||||
white = (245, 245, 250)
|
||||
gray = (150, 155, 170)
|
||||
dim = (80, 85, 100)
|
||||
|
||||
img = Image.new("RGB", (self.WIDTH, self.HEIGHT), dark1)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
self._draw_gradient_rect(draw, (0, 0, self.WIDTH, self.HEIGHT), self.COLORS["dark"], self.COLORS["mid"])
|
||||
self._gradient(draw, (0, 0, self.WIDTH, self.HEIGHT), dark1, dark2)
|
||||
|
||||
self._draw_wave(draw, 120, (*self.COLORS["accent"][:3],), amplitude=20, segments=10)
|
||||
self._draw_wave(draw, 140, (*self.COLORS["accent_dim"][:3],), amplitude=15, segments=12)
|
||||
self._draw_wave(draw, 360, (*self.COLORS["light"][:3],), amplitude=12, segments=8)
|
||||
self._draw_security_pattern(draw, self.WIDTH, self.HEIGHT)
|
||||
|
||||
self._rounded_rect(draw, (20, 20, self.WIDTH - 20, self.HEIGHT - 20), self.RADIUS, fill=None, outline=self.COLORS["accent"], width=2)
|
||||
for wave_y, color, amp, seg in [
|
||||
(100, (*accent[:3],), 25, 10),
|
||||
(130, (*accent_dim[:3],), 18, 12),
|
||||
(350, (55, 60, 85), 12, 8),
|
||||
]:
|
||||
pts = []
|
||||
for i in range(seg + 1):
|
||||
px = int(self.WIDTH * i / seg)
|
||||
py = wave_y + int(amp * math.sin(i * 1.3))
|
||||
pts.append((px, py))
|
||||
for i in range(len(pts) - 1):
|
||||
draw.line([pts[i], pts[i + 1]], fill=color, width=2)
|
||||
|
||||
self._draw_chip(draw, 60, 130)
|
||||
self._rounded_rect(draw, (18, 18, self.WIDTH - 18, self.HEIGHT - 18), 28, outline=accent, width=2)
|
||||
|
||||
draw.text((75, 185), "VICTORIA'S VICE", fill=self.COLORS["accent"], font=self.font_title)
|
||||
draw.text((75, 218), bank_name.upper(), fill=self.COLORS["gray"], font=self.font_label)
|
||||
img = self._draw_glow(img, 85, 155, radius=70, color=accent, intensity=20)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
self._draw_chip(draw, 55, 125)
|
||||
self._draw_hologram(draw, self.WIDTH - 120, 120, size=55)
|
||||
self._draw_contactless(draw, self.WIDTH - 80, 80)
|
||||
|
||||
draw.text((65, 182), "VICTORIA'S VICE", fill=accent, font=self.font_title)
|
||||
draw.text((65, 215), bank_name.upper(), fill=gray, font=self.font_label)
|
||||
|
||||
type_label = "COMPTE ENTREPRISE" if account_type == "entreprise" else "COMPTE PARTICULIER"
|
||||
draw.text((self.WIDTH - 200, 45), type_label, fill=self.COLORS["dim"], font=self.font_label)
|
||||
draw.text((self.WIDTH - 210, 42), type_label, fill=dim, font=self.font_label)
|
||||
|
||||
y_rib = 280
|
||||
draw.text((60, y_rib), "RIB", fill=self.COLORS["dim"], font=self.font_label)
|
||||
|
||||
rib_parts = []
|
||||
clean_rib = rib.replace(" ", "")
|
||||
for i in range(0, len(clean_rib), 4):
|
||||
rib_parts.append(clean_rib[i:i + 4])
|
||||
rib_display = " ".join(rib_parts)
|
||||
draw.text((60, y_rib + 22), rib_display, fill=self.COLORS["white"], font=self.font_rib)
|
||||
rib_display = " ".join(clean_rib[i:i + 4] for i in range(0, len(clean_rib), 4))
|
||||
|
||||
y_solde = 350
|
||||
draw.text((60, y_solde), "SOLDE", fill=self.COLORS["dim"], font=self.font_label)
|
||||
y_card_number = 260
|
||||
draw.text((60, y_card_number - 18), "RIB", fill=dim, font=self.font_label)
|
||||
draw.text((60, y_card_number), rib_display, fill=white, font=self.font_rib)
|
||||
|
||||
y_solde = 340
|
||||
draw.text((60, y_solde - 18), "SOLDE DISPONIBLE", fill=dim, font=self.font_label)
|
||||
solde_str = f"{solde:,.2f} EUR".replace(",", " ")
|
||||
draw.text((60, y_solde + 22), solde_str, fill=self.COLORS["gold"], font=self.font_large)
|
||||
|
||||
y_bottom = self.HEIGHT - 100
|
||||
draw.text((60, y_bottom), "TITULAIRE", fill=self.COLORS["dim"], font=self.font_label)
|
||||
draw.text((60, y_bottom + 20), holder_name.upper(), fill=self.COLORS["white"], font=self.font_value)
|
||||
draw.text((60, y_solde), solde_str, fill=gold, font=self.font_large)
|
||||
|
||||
if account_type == "entreprise" and tva_rate > 0:
|
||||
draw.text((self.WIDTH - 200, y_bottom), "TVA", fill=self.COLORS["dim"], font=self.font_label)
|
||||
draw.text((self.WIDTH - 200, y_bottom + 20), f"{tva_rate}%", fill=self.COLORS["accent"], font=self.font_value)
|
||||
draw.text((self.WIDTH - 180, y_solde - 18), "TAUX TVA", fill=dim, font=self.font_label)
|
||||
draw.text((self.WIDTH - 180, y_solde), f"{tva_rate}%", fill=accent, font=self.font_value)
|
||||
|
||||
for i in range(5):
|
||||
x = self.WIDTH - 60 - i * 35
|
||||
draw.ellipse([x, self.HEIGHT - 50, x + 25, self.HEIGHT - 25], fill=self.COLORS["accent"] if i < 3 else self.COLORS["dim"])
|
||||
y_bottom = self.HEIGHT - 110
|
||||
draw.line([(60, y_bottom - 10), (self.WIDTH - 60, y_bottom - 10)], fill=(60, 65, 85), width=1)
|
||||
|
||||
draw.text((60, y_bottom), "TITULAIRE", fill=dim, font=self.font_label)
|
||||
draw.text((60, y_bottom + 18), holder_name.upper(), fill=white, font=self.font_value)
|
||||
|
||||
sig_y = y_bottom + 5
|
||||
for i in range(40):
|
||||
sx = self.WIDTH - 250 + i * 3
|
||||
sy = sig_y + int(3 * math.sin(i * 0.5))
|
||||
draw.point((sx, sy), fill=(120, 125, 140))
|
||||
draw.point((sx, sy + 1), fill=(120, 125, 140))
|
||||
|
||||
for i in range(4):
|
||||
cx = self.WIDTH - 65 - i * 30
|
||||
draw.ellipse([cx, self.HEIGHT - 45, cx + 28, self.HEIGHT - 17], fill=accent if i < 2 else dim)
|
||||
|
||||
draw.text((55, self.HEIGHT - 35), "VIRTUAL CARD", fill=dim, font=self.font_small)
|
||||
|
||||
buf = BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
|
|
|
|||
188
utils/bank_statement.py
Normal file
188
utils/bank_statement.py
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue