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:
Lizandro GD
2026-08-03 00:45:58 +00:00
co-authored by Claude Sonnet 5
parent 772a4a5656
commit 516220a8a1
32 changed files with 2272 additions and 132 deletions
+231 -9
View File
@@ -165,6 +165,17 @@ func agentTools() []agentTool {
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
tool("enviar_correo_contrato", "Envía notificación de renovación por correo al cliente.",
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
tool("crear_contrato", "Crea un contrato para un cliente con uno o más servicios y genera de una vez su PDF con cláusulas estándar.",
obj(map[string]agentToolParam{
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
"servicio_ids": agentToolParam{Type: "array", Description: "IDs de los servicios contratados", Items: &agentToolParam{Type: "number"}},
"duracion_meses": num("Duración del contrato en meses (default 12)"),
"precio_acordado": num("Valor total acordado"),
"moneda": str("Moneda, ej: COP, USD (default COP)"),
"notas": str("Notas adicionales del contrato"),
}, []string{"cliente_id", "servicio_ids", "precio_acordado"})),
tool("generar_documento_contrato", "Genera (o regenera) el PDF de un contrato ya existente a partir de la plantilla activa.",
obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
// ── Contabilidad ─────────────────────────────────────────────────────
tool("dashboard_contabilidad", "Resumen del mes: ingresos, egresos, pendientes.",
@@ -192,18 +203,72 @@ func agentTools() []agentTool {
"page": num("Página"),
"estado": str("pendiente | pagada | vencida"),
}, nil)),
tool("crear_cuenta_cobro", "Crea una cuenta de cobro para un cliente/proyecto y genera de una vez su PDF de solicitud.",
obj(map[string]agentToolParam{
"cliente_id": num("ID del cliente"),
"descripcion": str("Descripción del cobro, ej: proyecto y periodo"),
"valor": num("Valor a cobrar"),
"notas": str("Notas adicionales"),
}, []string{"cliente_id", "descripcion", "valor"})),
tool("generar_documento_cuenta_cobro", "Genera (o regenera) el PDF de una cuenta de cobro ya existente.",
obj(map[string]agentToolParam{"id": num("ID de la cuenta de cobro")}, []string{"id"})),
tool("listar_cuentas_pagar", "Lista cuentas por pagar (lo que debemos).",
obj(map[string]agentToolParam{
"page": num("Página"),
"estado": str("pendiente | pagada | vencida"),
}, nil)),
// ── Automatización IA: cotizaciones y tarifas ───────────────────────────
tool("listar_tarifas", "Lista las tarifas activas (valor por hora, licencias, VMs, márgenes) para armar cotizaciones.",
obj(map[string]agentToolParam{
"categoria": str("Filtrar por categoría: hora_servicio | licencia | vm_azure | margen | otro"),
}, nil)),
tool("crear_cotizacion", "Genera el PDF de una cotización para un cliente, a partir de la plantilla activa y los items con precio ya calculado (consulta listar_tarifas antes para saber los valores).",
obj(map[string]agentToolParam{
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
"alcance": str("Descripción del alcance del proyecto o servicio a cotizar"),
"tipo_proyecto": str("Tipo de proyecto, ej: migracion_m365, vm_azure, soporte"),
"items": agentToolParam{
Type: "array",
Description: "Ítems de la cotización con precio ya calculado",
Items: &agentToolParam{
Type: "object",
Properties: map[string]agentToolParam{
"descripcion": str("Descripción del ítem"),
"cantidad": num("Cantidad"),
"valor_unitario": num("Valor unitario en COP"),
"unidad": str("hora | mes | unico"),
},
Required: []string{"descripcion", "cantidad", "valor_unitario"},
},
},
}, []string{"cliente_id", "alcance", "items"})),
// ── Automatización IA: arquitecturas ────────────────────────────────────
tool("listar_arquitecturas_referencia", "Lista los patrones de arquitectura técnica ya resueltos (ej: AD redundante en Azure con VPN Gateway) para reutilizarlos en vez de improvisar.",
obj(map[string]agentToolParam{"search": str("Búsqueda por nombre o tag")}, nil)),
tool("generar_arquitectura", "Genera el PDF de una propuesta técnica ya redactada (consulta antes listar_arquitecturas_referencia para reutilizar patrones existentes).",
obj(map[string]agentToolParam{
"requerimiento": str("Requerimiento del cliente en texto libre"),
"propuesta": str("Propuesta técnica ya redactada (HTML o texto) que se insertará en el documento"),
"nombre": str("Título de la propuesta"),
"cliente_id": num("ID del cliente (opcional)"),
"guardar_como_referencia": agentToolParam{Type: "boolean", Description: "Si true, guarda esta propuesta como patrón reutilizable para el futuro"},
}, []string{"requerimiento", "propuesta"})),
// ── Proyectos ────────────────────────────────────────────────────────
tool("listar_proyectos", "Lista proyectos.",
obj(map[string]agentToolParam{
"page": num("Página"),
"search": str("Búsqueda"),
}, nil)),
tool("crear_proyecto", "Crea un nuevo proyecto para un cliente.",
obj(map[string]agentToolParam{
"cliente_id": num("ID del cliente (usa listar_clientes si no lo sabes)"),
"nombre": str("Nombre del proyecto"),
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
"descripcion": str("Descripción adicional del proyecto"),
}, []string{"cliente_id", "nombre"})),
// ── Tickets ──────────────────────────────────────────────────────────
tool("listar_tickets", "Lista tickets de soporte de todos los proyectos.",
@@ -294,9 +359,9 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
names[i] = fmt.Sprintf("#%d %s (%s)", c.ID, c.Nombre, c.BaseURL)
}
return map[string]interface{}{
"sistema": "U-Site Admin",
"sistema": "U-Site Admin",
"coolify_instancias": names,
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
"timestamp": time.Now().Format("2006-01-02 15:04:05"),
}, nil
// ── Coolify ────────────────────────────────────────────────────────────
@@ -328,9 +393,9 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
return nil, fmt.Errorf("instancia Coolify #%d no encontrada", configID)
}
endpoint := map[string]string{
"coolify_apps": "/applications",
"coolify_servicios": "/services",
"coolify_servidores": "/servers",
"coolify_apps": "/applications",
"coolify_servicios": "/services",
"coolify_servidores": "/servers",
}[name]
if endpoint == "" {
uuid := getStr("uuid")
@@ -464,6 +529,51 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
"cliente_id": contrato.ClienteID,
}, nil
case "crear_contrato":
idsRaw, _ := a["servicio_ids"].([]interface{})
servicioIDs := make([]uint, 0, len(idsRaw))
for _, v := range idsRaw {
switch x := v.(type) {
case float64:
servicioIDs = append(servicioIDs, uint(x))
case int:
servicioIDs = append(servicioIDs, uint(x))
}
}
contrato, doc, err := CrearContratoConDocumento(
uint(getInt("cliente_id", 0)),
servicioIDs,
getInt("duracion_meses", 12),
float64(getInt("precio_acordado", 0)),
getStr("moneda"),
getStr("notas"),
"telegram",
)
if err != nil {
return nil, err
}
result := map[string]interface{}{"ok": true, "contrato_id": contrato.ID}
if doc != nil {
result["documento_id"] = doc.ID
result["descargar"] = fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID)
}
return result, nil
case "generar_documento_contrato":
id := uint(getInt("id", 0))
if id == 0 {
return nil, fmt.Errorf("id requerido")
}
doc, err := GenerarDocumentoContrato(id, "telegram")
if err != nil {
return nil, err
}
return map[string]interface{}{
"ok": true,
"documento_id": doc.ID,
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
}, nil
// ── Contabilidad ───────────────────────────────────────────────────────
case "dashboard_contabilidad":
now := time.Now()
@@ -511,6 +621,40 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
}
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
case "crear_cuenta_cobro":
cc, doc, err := CrearCuentaCobroConDocumento(
uint(getInt("cliente_id", 0)),
getStr("descripcion"),
float64(getInt("valor", 0)),
nil,
getStr("notas"),
"telegram",
)
if err != nil {
return nil, err
}
result := map[string]interface{}{"ok": true, "cuenta_cobro_id": cc.ID}
if doc != nil {
result["documento_id"] = doc.ID
result["descargar"] = fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID)
}
return result, nil
case "generar_documento_cuenta_cobro":
id := uint(getInt("id", 0))
if id == 0 {
return nil, fmt.Errorf("id requerido")
}
doc, err := GenerarDocumentoCuentaCobro(id, "telegram")
if err != nil {
return nil, err
}
return map[string]interface{}{
"ok": true,
"documento_id": doc.ID,
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
}, nil
case "listar_cuentas_pagar":
page := getInt("page", 1)
estado := getStr("estado")
@@ -522,6 +666,77 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
}
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
// ── Automatización IA: cotizaciones y tarifas ───────────────────────────
case "listar_tarifas":
items, err := models.GetTarifasActivas(getStr("categoria"))
if err != nil {
return nil, err
}
return items, nil
case "crear_cotizacion":
itemsRaw, _ := a["items"].([]interface{})
items := make([]ItemCotizacion, 0, len(itemsRaw))
for _, raw := range itemsRaw {
m, ok := raw.(map[string]interface{})
if !ok {
continue
}
toFloat := func(v interface{}) float64 {
switch x := v.(type) {
case float64:
return x
case int:
return float64(x)
}
return 0
}
toStr := func(v interface{}) string {
s, _ := v.(string)
return s
}
items = append(items, ItemCotizacion{
Descripcion: toStr(m["descripcion"]),
Cantidad: toFloat(m["cantidad"]),
ValorUnitario: toFloat(m["valor_unitario"]),
Unidad: toStr(m["unidad"]),
})
}
doc, total, err := CrearCotizacion(uint(getInt("cliente_id", 0)), getStr("alcance"), getStr("tipo_proyecto"), items, "telegram")
if err != nil {
return nil, err
}
return map[string]interface{}{
"ok": true,
"documento_id": doc.ID,
"total": total,
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
}, nil
// ── Automatización IA: arquitecturas ────────────────────────────────────
case "listar_arquitecturas_referencia":
items, _, err := models.GetAllArquitecturas(50, 0, getStr("search"), true)
if err != nil {
return nil, err
}
return items, nil
case "generar_arquitectura":
var clienteID *uint
if id := getInt("cliente_id", 0); id > 0 {
u := uint(id)
clienteID = &u
}
doc, err := GenerarArquitectura(getStr("requerimiento"), getStr("propuesta"), getStr("nombre"), clienteID, getBool("guardar_como_referencia"), "telegram")
if err != nil {
return nil, err
}
return map[string]interface{}{
"ok": true,
"documento_id": doc.ID,
"descargar": fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
}, nil
// ── Proyectos ──────────────────────────────────────────────────────────
case "listar_proyectos":
page := getInt("page", 1)
@@ -534,6 +749,13 @@ func runTool(name string, a map[string]interface{}) (interface{}, error) {
}
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
case "crear_proyecto":
p, err := CrearProyectoSimple(uint(getInt("cliente_id", 0)), getStr("nombre"), getStr("stack"), getStr("descripcion"))
if err != nil {
return nil, err
}
return map[string]interface{}{"ok": true, "id": p.ID, "slug": p.Slug}, nil
// ── Tickets ────────────────────────────────────────────────────────────
case "listar_tickets":
estado := getStr("estado")
@@ -699,10 +921,10 @@ type anthropicMsg struct {
}
type anthropicReq struct {
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"`
Messages []anthropicMsg `json:"messages"`
Model string `json:"model"`
MaxTokens int `json:"max_tokens"`
System string `json:"system,omitempty"`
Messages []anthropicMsg `json:"messages"`
Tools []anthropicTool `json:"tools,omitempty"`
}