El motor de PDF ya existía entero para el staff — Chrome headless, text/template
con {{range .Items}}, y hasta el importador que convierte un Word en plantilla
con IA. Lo único que faltaba era que fueran de cada cliente.
PlantillaDocumento gana TenantID *uint: nulo = global del staff (lo de
siempre), con valor = del espacio. Mismo patrón exacto que AiConfig, el que ya
tiene su lección aprendida. Y GetPlantillaDocumentoActiva ahora filtra
tenant_id IS NULL explícitamente: sin eso, la plantilla que un cliente escribe
para su propio contrato podía salir en un documento de la empresa. Hay test.
Un cliente sin plantilla propia cae a la global, así puede emitir una
cotización desde el primer día y personalizarla cuando quiera. Guardar crea
versión nueva en vez de pisar la vieja: si la nueva sale mal, la anterior sigue
ahí. Y se valida que compile ANTES de guardar — una plantilla rota descubierta
al generar deja al cliente esperando un PDF que nunca llega.
La tool solo se le ofrece al modelo si hay alguna plantilla disponible:
prometerle una capacidad que después falla es peor que no tenerla.
El semáforo de tres: cada PDF levanta un Chrome entero, y cien clientes
generando a la vez son cien navegadores. Eso tira el servidor mucho antes que
cualquier consulta a la IA, así que va desde el día uno y no cuando se caiga.
El JSON de ítems mal formado no tumba la generación — sale el documento sin la
tabla, que todavía se puede corregir a mano. Y sin cantidad se asume 1: el
modelo la omite seguido, y un total en cero es peor que uno aproximado.
Cada documento se cobra (levanta un Chrome) y queda en los archivos del
espacio, descargable como cualquier otro.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
130 lines
5.0 KiB
Go
130 lines
5.0 KiB
Go
package models
|
|
|
|
import (
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PlantillaDocumento es la fuente de verdad de las plantillas base usadas por
|
|
// la automatización con IA: cotización, contrato, acta de proyecto y cuenta de cobro.
|
|
// Editar una plantilla aquí actualiza automáticamente todos los canales (Telegram,
|
|
// chat propio, Claude directo) que generan ese tipo de documento.
|
|
type PlantillaDocumento struct {
|
|
gorm.Model
|
|
Tipo string `json:"tipo" gorm:"column:tipo;size:30;not null;index"` // cotizacion | contrato | acta | cuenta_cobro
|
|
Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"`
|
|
ContenidoHTML string `json:"contenido_html" gorm:"column:contenido_html;type:text"` // Go text/template
|
|
Version int `json:"version" gorm:"column:version;default:1"`
|
|
Activa bool `json:"activa" gorm:"column:activa;default:true"`
|
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
|
// TenantID separa las plantillas de cada cliente de las globales del
|
|
// staff. Nulo = global (el comportamiento de siempre). Mismo patrón que
|
|
// AiConfig — el que ya tiene su lección aprendida y su test que la cuida.
|
|
TenantID *uint `json:"tenant_id" gorm:"column:tenant_id;index"`
|
|
}
|
|
|
|
func (PlantillaDocumento) TableName() string { return "plantillas_documento" }
|
|
|
|
func GetAllPlantillasDocumento(limit, offset int, search, tipo string) ([]PlantillaDocumento, int64, error) {
|
|
var items []PlantillaDocumento
|
|
var total int64
|
|
db := app.Http.Database.DB.Model(&PlantillaDocumento{})
|
|
if search != "" {
|
|
db = db.Where("nombre ILIKE ?", "%"+search+"%")
|
|
}
|
|
if tipo != "" {
|
|
db = db.Where("tipo = ?", tipo)
|
|
}
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := db.Order("tipo ASC, version DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func GetPlantillaDocumentoByID(id uint) (*PlantillaDocumento, error) {
|
|
var item PlantillaDocumento
|
|
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
// GetPlantillaDocumentoActiva retorna la plantilla activa más reciente para un tipo dado.
|
|
// Es la que usan los endpoints de generación (cotizaciones, contratos, etc).
|
|
// GetPlantillaDocumentoActiva devuelve la plantilla GLOBAL del staff. Filtra
|
|
// tenant_id IS NULL a propósito: sin eso, la plantilla que un cliente escribe
|
|
// para su propio contrato podría salir en un documento de la empresa.
|
|
func GetPlantillaDocumentoActiva(tipo string) (*PlantillaDocumento, error) {
|
|
var item PlantillaDocumento
|
|
if err := app.Http.Database.DB.
|
|
Where("tipo = ? AND activa = ? AND tenant_id IS NULL", tipo, true).
|
|
Order("version DESC").
|
|
First(&item).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
// GetPlantillaDocumentoDeTenant busca la plantilla propia del espacio y, si no
|
|
// tiene una para ese tipo, cae a la global. Así un cliente nuevo puede emitir
|
|
// una cotización desde el primer día y personalizarla cuando quiera.
|
|
func GetPlantillaDocumentoDeTenant(tenantID uint, tipo string) (*PlantillaDocumento, error) {
|
|
var item PlantillaDocumento
|
|
err := app.Http.Database.DB.
|
|
Where("tipo = ? AND activa = ? AND tenant_id = ?", tipo, true, tenantID).
|
|
Order("version DESC").First(&item).Error
|
|
if err == nil {
|
|
return &item, nil
|
|
}
|
|
return GetPlantillaDocumentoActiva(tipo)
|
|
}
|
|
|
|
// GetPlantillasDeTenant lista las propias del espacio, sin las globales.
|
|
func GetPlantillasDeTenant(tenantID uint) ([]PlantillaDocumento, error) {
|
|
var items []PlantillaDocumento
|
|
err := app.Http.Database.DB.Where("tenant_id = ?", tenantID).
|
|
Order("tipo ASC, version DESC").Limit(100).Find(&items).Error
|
|
return items, err
|
|
}
|
|
|
|
func CreatePlantillaDocumento(p PlantillaDocumento) error {
|
|
return app.Http.Database.DB.Create(&p).Error
|
|
}
|
|
|
|
func UpdatePlantillaDocumento(id uint, updates map[string]interface{}) error {
|
|
return app.Http.Database.DB.Model(&PlantillaDocumento{}).Where("id = ?", id).Updates(updates).Error
|
|
}
|
|
|
|
func DeletePlantillaDocumento(id uint) error {
|
|
return app.Http.Database.DB.Delete(&PlantillaDocumento{}, id).Error
|
|
}
|
|
|
|
// TienePlantillasDocumento dice si el agente puede emitir documentos. Sin
|
|
// plantilla la tool no se le ofrece al modelo: prometerle una capacidad que
|
|
// después falla es peor que no tenerla.
|
|
func TienePlantillasDocumento(agenteID uint) bool {
|
|
agente, err := GetUmindAgenteByID(agenteID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
var n int64
|
|
app.Http.Database.DB.Model(&PlantillaDocumento{}).
|
|
Where("activa = ? AND deleted_at IS NULL AND (tenant_id = ? OR tenant_id IS NULL)", true, agente.TenantID).
|
|
Count(&n)
|
|
return n > 0
|
|
}
|
|
|
|
// SiguienteVersionPlantilla numera la nueva versión del cliente. Se guarda
|
|
// versión nueva en vez de pisar la vieja: si la nueva sale mal, la anterior
|
|
// sigue ahí.
|
|
func SiguienteVersionPlantilla(tenantID uint, tipo string) int {
|
|
var max int
|
|
app.Http.Database.DB.Model(&PlantillaDocumento{}).
|
|
Where("tenant_id = ? AND tipo = ?", tenantID, tipo).
|
|
Select("COALESCE(MAX(version), 0)").Scan(&max)
|
|
return max + 1
|
|
}
|