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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
772a4a5656
commit
516220a8a1
@@ -0,0 +1,82 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user