import io import base64 from PIL import Image, ImageDraw, ImageFont def generate_quote_image(quote_items, totals, tenant_name="Autopartes", logo_text="NEXUS"): """ Generate a visually appealing quote image. quote_items: list of dicts with keys: name, sku, qty, price, total totals: dict with keys: subtotal, tax, total Returns: base64 encoded PNG string """ # Dimensions WIDTH = 800 HEADER_H = 120 FOOTER_H = 100 ITEM_H = 60 PADDING = 30 total_height = HEADER_H + len(quote_items) * ITEM_H + FOOTER_H + PADDING * 3 # Colors BG_COLOR = (250, 250, 252) PRIMARY = (0, 82, 155) # Dark blue ACCENT = (230, 57, 70) # Red accent TEXT_DARK = (30, 30, 30) TEXT_MED = (80, 80, 80) TEXT_LIGHT = (150, 150, 150) WHITE = (255, 255, 255) ROW_ALT = (245, 247, 250) img = Image.new('RGB', (WIDTH, total_height), BG_COLOR) draw = ImageDraw.Draw(img) # Try to load fonts, fallback to default try: font_title = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 32) font_sub = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 18) font_item = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20) font_bold = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 22) font_small = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 14) except Exception: font_title = ImageFont.load_default() font_sub = font_title font_item = font_title font_bold = font_title font_small = font_title # --- Header --- draw.rectangle([0, 0, WIDTH, HEADER_H], fill=PRIMARY) # Logo text draw.text((PADDING, 25), logo_text, font=font_title, fill=WHITE) draw.text((PADDING, 70), tenant_name, font=font_sub, fill=(200, 210, 230)) # Date and Quote label from datetime import datetime date_str = datetime.now().strftime("%d/%m/%Y %H:%M") draw.text((WIDTH - PADDING - 200, 30), "COTIZACIÓN", font=font_title, fill=WHITE) draw.text((WIDTH - PADDING - 200, 75), date_str, font=font_sub, fill=(200, 210, 230)) # --- Items Header --- y = HEADER_H + PADDING draw.rectangle([PADDING, y, WIDTH - PADDING, y + ITEM_H], fill=(230, 235, 240)) draw.text((PADDING + 10, y + 18), "PRODUCTO", font=font_bold, fill=TEXT_DARK) draw.text((WIDTH - PADDING - 220, y + 18), "CANT.", font=font_bold, fill=TEXT_DARK) draw.text((WIDTH - PADDING - 130, y + 18), "P.UNIT", font=font_bold, fill=TEXT_DARK) draw.text((WIDTH - PADDING - 50, y + 18), "TOTAL", font=font_bold, fill=TEXT_DARK) y += ITEM_H # --- Items --- for idx, item in enumerate(quote_items): row_y = y + idx * ITEM_H bg = ROW_ALT if idx % 2 == 0 else WHITE draw.rectangle([PADDING, row_y, WIDTH - PADDING, row_y + ITEM_H], fill=bg) name = item.get('name', 'Producto') sku = item.get('sku', '') qty = str(item.get('qty', 1)) price = f"${item.get('price', 0):,.2f}" total = f"${item.get('total', 0):,.2f}" # Truncate name if too long name_display = name if len(name_display) > 35: name_display = name_display[:32] + "..." draw.text((PADDING + 10, row_y + 8), name_display, font=font_item, fill=TEXT_DARK) draw.text((PADDING + 10, row_y + 32), f"SKU: {sku}", font=font_small, fill=TEXT_MED) draw.text((WIDTH - PADDING - 220, row_y + 18), qty, font=font_item, fill=TEXT_DARK) draw.text((WIDTH - PADDING - 130, row_y + 18), price, font=font_item, fill=TEXT_DARK) draw.text((WIDTH - PADDING - 50, row_y + 18), total, font=font_item, fill=TEXT_DARK) y += len(quote_items) * ITEM_H + PADDING # --- Totals --- draw.line([(PADDING, y), (WIDTH - PADDING, y)], fill=(200, 200, 200), width=2) y += 20 subtotal = totals.get('subtotal', 0) tax = totals.get('tax', 0) total = totals.get('total', 0) draw.text((WIDTH - PADDING - 300, y), "Subtotal:", font=font_sub, fill=TEXT_MED) draw.text((WIDTH - PADDING - 50, y), f"${subtotal:,.2f}", font=font_sub, fill=TEXT_DARK) y += 30 draw.text((WIDTH - PADDING - 300, y), "IVA (16%):", font=font_sub, fill=TEXT_MED) draw.text((WIDTH - PADDING - 50, y), f"${tax:,.2f}", font=font_sub, fill=TEXT_DARK) y += 35 draw.text((WIDTH - PADDING - 300, y), "TOTAL:", font=font_bold, fill=ACCENT) draw.text((WIDTH - PADDING - 50, y), f"${total:,.2f}", font=font_bold, fill=ACCENT) y += 50 # --- Footer --- draw.rectangle([0, total_height - FOOTER_H, WIDTH, total_height], fill=PRIMARY) footer_text = "Validez: 5 días hábiles | Envíos a todo México | Contacto: ventas@nexusautoparts.com" draw.text((PADDING, total_height - FOOTER_H + 35), footer_text, font=font_small, fill=(200, 210, 230)) # Convert to base64 buffer = io.BytesIO() img.save(buffer, format='PNG') buffer.seek(0) return base64.b64encode(buffer.read()).decode('utf-8')