Files
Autoparts-DB/pos/services/quote_image.py
consultoria-as a236187f3a feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes
- 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
2026-05-26 04:24:07 +00:00

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')