Files
Lizandro GDandClaude Sonnet 5 516220a8a1 feat: automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
Implementa las 4 fases de la especificación de automatización: módulo de
plantillas/tarifas editable por el equipo, generación de PDF (HTML+JS vía
Chrome headless) para cotizaciones/contratos/arquitecturas/cuentas de cobro,
chat propio en el dashboard reutilizando el mismo motor y tools del bot de
Telegram, y nuevas tools del agente para crear estos documentos end-to-end.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 00:45:58 +00:00

83 lines
2.6 KiB
Go

package services
import (
"context"
"encoding/base64"
"fmt"
"os"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
// RenderHTMLToPDF convierte un documento HTML (ya renderizado, con JS/CSS inline si aplica)
// a PDF usando Chrome headless. Es el motor de "impresión" que comparten cotizaciones,
// contratos, actas de proyecto y cuentas de cobro: la plantilla se renderiza a HTML con
// text/template y este servicio la convierte al PDF final.
//
// Requiere Chrome/Chromium instalado en el host (ver Dockerfile). La ruta del binario se
// puede forzar con la variable de entorno CHROME_EXEC_PATH; si no está definida, chromedp
// intenta ubicar un Chrome instalado en el sistema.
func RenderHTMLToPDF(html string) ([]byte, error) {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("disable-dev-shm-usage", true),
)
if execPath := os.Getenv("CHROME_EXEC_PATH"); execPath != "" {
opts = append(opts, chromedp.ExecPath(execPath))
}
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancelAlloc()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ctx, cancelTimeout := context.WithTimeout(ctx, 30*time.Second)
defer cancelTimeout()
dataURI := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(html))
var pdfBuf []byte
err := chromedp.Run(ctx,
chromedp.Navigate(dataURI),
chromedp.WaitReady("body", chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
buf, _, err := page.PrintToPDF().
WithPrintBackground(true).
WithPreferCSSPageSize(true).
WithMarginTop(0.4).
WithMarginBottom(0.4).
WithMarginLeft(0.4).
WithMarginRight(0.4).
Do(ctx)
if err != nil {
return err
}
pdfBuf = buf
return nil
}),
)
if err != nil {
return nil, fmt.Errorf("error generando PDF: %w", err)
}
return pdfBuf, nil
}
// SaveGeneratedFile persiste bytes generados (ej. un PDF) en disco bajo uploads/documentos/{subdir}
// y retorna la ruta relativa guardada en el modelo (DocumentoGenerado.Archivo, ProyectoDocumento.Archivo, etc).
func SaveGeneratedFile(subdir, filename string, data []byte) (string, error) {
dir := fmt.Sprintf("uploads/documentos/%s", subdir)
if err := os.MkdirAll(dir, 0755); err != nil {
return "", fmt.Errorf("no se pudo crear el directorio %s: %w", dir, err)
}
path := dir + "/" + filename
if err := os.WriteFile(path, data, 0644); err != nil {
return "", fmt.Errorf("no se pudo escribir %s: %w", path, err)
}
return path, nil
}