- Add MercadoLibre OAuth, listings, orders, webhooks and category search - New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py - New marketplace_external.html/js with ML management UI - Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors - Inventory: new .btn--meli styles, select/label CSS fixes - WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog - DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue - Add Celery tasks for ML sync and webhook processing - Sidebar: MercadoLibre navigation link
128 lines
5.1 KiB
Python
128 lines
5.1 KiB
Python
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')
|
|
|