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
+14
-3
@@ -24,11 +24,11 @@ type AiConfig struct {
|
||||
// "" = global (disponible para todos como fallback)
|
||||
// "landing" = exclusivo para Landing Generator
|
||||
// "query_runner" = exclusivo para Query Runner SQL
|
||||
Modulo string `gorm:"size:50;default:''" json:"modulo"`
|
||||
Modulo string `gorm:"size:50;default:''" json:"modulo"`
|
||||
// Agente Telegram: si EsAgenteBot=true, esta config es el cerebro del bot administrador.
|
||||
// Solo debe haber una config activa como agente a la vez.
|
||||
EsAgenteBot bool `gorm:"default:false" json:"es_agente_bot"`
|
||||
TelegramConfigID *uint `gorm:"index" json:"telegram_config_id"`
|
||||
EsAgenteBot bool `gorm:"default:false" json:"es_agente_bot"`
|
||||
TelegramConfigID *uint `gorm:"index" json:"telegram_config_id"`
|
||||
}
|
||||
|
||||
func (AiConfig) TableName() string { return "ai_configs" }
|
||||
@@ -127,6 +127,17 @@ func GetAgenteBotConfig() (*AiConfig, *TelegramConfig, error) {
|
||||
return &ai, tg, nil
|
||||
}
|
||||
|
||||
// GetAgenteBotAiConfig retorna solo la config de IA marcada como agente (el mismo
|
||||
// "cerebro" que usa el bot de Telegram), sin exigir que tenga un bot de Telegram
|
||||
// asignado. La usa el chat propio del dashboard para compartir el mismo motor.
|
||||
func GetAgenteBotAiConfig() (*AiConfig, error) {
|
||||
var ai AiConfig
|
||||
if err := app.Http.Database.DB.Where("es_agente_bot = ? AND is_active = ?", true, true).First(&ai).Error; err != nil {
|
||||
return nil, fmt.Errorf("no hay agente configurado: %w", err)
|
||||
}
|
||||
return &ai, nil
|
||||
}
|
||||
|
||||
// GetAiConfigForService retorna la config activa asignada al módulo indicado.
|
||||
// Lógica de prioridad:
|
||||
// 1. Config activa con modulo conteniendo service (puede ser comma-separated)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Arquitectura guarda patrones de arquitectura técnica ya resueltos (ej: Active
|
||||
// Directory redundante en Azure con VPN Gateway) para que la IA los reutilice como
|
||||
// referencia al generar una propuesta técnica nueva, en vez de improvisar cada vez.
|
||||
type Arquitectura struct {
|
||||
gorm.Model
|
||||
Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"`
|
||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||
ContenidoHTML string `json:"contenido_html" gorm:"column:contenido_html;type:text"` // diagrama/detalle en HTML, reutilizable como referencia
|
||||
Tags string `json:"tags" gorm:"column:tags;size:255"` // comma-separated, ej: "azure,ad,vpn"
|
||||
EsReferencia bool `json:"es_referencia" gorm:"column:es_referencia;default:true"` // true = patrón reutilizable, false = propuesta generada puntual
|
||||
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"`
|
||||
}
|
||||
|
||||
func (Arquitectura) TableName() string { return "arquitecturas" }
|
||||
|
||||
func GetAllArquitecturas(limit, offset int, search string, soloReferencias bool) ([]Arquitectura, int64, error) {
|
||||
var items []Arquitectura
|
||||
var total int64
|
||||
db := app.Http.Database.DB.Model(&Arquitectura{})
|
||||
if search != "" {
|
||||
db = db.Where("nombre ILIKE ? OR tags ILIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
if soloReferencias {
|
||||
db = db.Where("es_referencia = ?", true)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := db.Order("nombre ASC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func GetArquitecturaByID(id uint) (*Arquitectura, error) {
|
||||
var item Arquitectura
|
||||
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func CreateArquitectura(a *Arquitectura) error {
|
||||
return app.Http.Database.DB.Create(a).Error
|
||||
}
|
||||
|
||||
func UpdateArquitectura(id uint, updates map[string]interface{}) error {
|
||||
return app.Http.Database.DB.Model(&Arquitectura{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func DeleteArquitectura(id uint) error {
|
||||
return app.Http.Database.DB.Delete(&Arquitectura{}, id).Error
|
||||
}
|
||||
+93
-66
@@ -61,19 +61,19 @@ func (Transaccion) TableName() string { return "contab_transacciones" }
|
||||
|
||||
type CuentaCobro struct {
|
||||
gorm.Model
|
||||
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;index;not null"`
|
||||
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
||||
EntidadID *uint `json:"entidad_id" gorm:"column:entidad_id;index"`
|
||||
Entidad *Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
||||
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
||||
FechaVencimiento *time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
|
||||
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
||||
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;index;not null"`
|
||||
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
||||
EntidadID *uint `json:"entidad_id" gorm:"column:entidad_id;index"`
|
||||
Entidad *Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
||||
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
||||
FechaVencimiento *time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
|
||||
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
||||
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||
}
|
||||
|
||||
func (CuentaCobro) TableName() string { return "contab_cuentas_cobro" }
|
||||
@@ -82,17 +82,17 @@ func (CuentaCobro) TableName() string { return "contab_cuentas_cobro" }
|
||||
|
||||
type CuentaPagar struct {
|
||||
gorm.Model
|
||||
EntidadID uint `json:"entidad_id" gorm:"column:entidad_id;index;not null"`
|
||||
Entidad Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
||||
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||
Vencimiento *time.Time `json:"vencimiento" gorm:"column:vencimiento"`
|
||||
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
||||
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
||||
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||
EntidadID uint `json:"entidad_id" gorm:"column:entidad_id;index;not null"`
|
||||
Entidad Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
||||
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
||||
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
||||
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||
Vencimiento *time.Time `json:"vencimiento" gorm:"column:vencimiento"`
|
||||
Estado string `json:"estado" gorm:"column:estado;size:20;default:'pendiente'"` // pendiente | pagado | parcial
|
||||
FechaPago *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id" gorm:"column:transaccion_id"`
|
||||
Transaccion *Transaccion `json:"transaccion" gorm:"foreignKey:TransaccionID"`
|
||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||
}
|
||||
|
||||
func (CuentaPagar) TableName() string { return "contab_cuentas_pagar" }
|
||||
@@ -101,12 +101,12 @@ func (CuentaPagar) TableName() string { return "contab_cuentas_pagar" }
|
||||
|
||||
type ConsolidadoMensual struct {
|
||||
gorm.Model
|
||||
Anio int `json:"anio" gorm:"column:anio;not null"`
|
||||
Mes int `json:"mes" gorm:"column:mes;not null"`
|
||||
TotalIngresos float64 `json:"total_ingresos" gorm:"column:total_ingresos;default:0"`
|
||||
TotalEgresos float64 `json:"total_egresos" gorm:"column:total_egresos;default:0"`
|
||||
Anio int `json:"anio" gorm:"column:anio;not null"`
|
||||
Mes int `json:"mes" gorm:"column:mes;not null"`
|
||||
TotalIngresos float64 `json:"total_ingresos" gorm:"column:total_ingresos;default:0"`
|
||||
TotalEgresos float64 `json:"total_egresos" gorm:"column:total_egresos;default:0"`
|
||||
TotalRetenciones float64 `json:"total_retenciones" gorm:"column:total_retenciones;default:0"`
|
||||
Resultado float64 `json:"resultado" gorm:"column:resultado;default:0"`
|
||||
Resultado float64 `json:"resultado" gorm:"column:resultado;default:0"`
|
||||
}
|
||||
|
||||
func (ConsolidadoMensual) TableName() string { return "contab_consolidado" }
|
||||
@@ -307,7 +307,11 @@ func SeedBalanceData() {
|
||||
}
|
||||
|
||||
// ─── FACTURAS → Transacciones (ingresos) ────────────────────────────────
|
||||
facturas := []struct{ factura float64; valor float64; fecha string }{
|
||||
facturas := []struct {
|
||||
factura float64
|
||||
valor float64
|
||||
fecha string
|
||||
}{
|
||||
{54, 450000, "2026-01-01 00:00:00"},
|
||||
{55, 9000000, "2026-01-01 00:00:00"},
|
||||
{56, 119000, "2026-01-01 00:00:00"},
|
||||
@@ -347,7 +351,7 @@ func SeedBalanceData() {
|
||||
if db.Where("descripcion = ?", "Ajuste/NC ene 2026").First(&existing).Error != nil {
|
||||
db.Create(&Transaccion{
|
||||
Fecha: parseDate("2026-01-01 00:00:00"),
|
||||
Tipo: "egreso", Descripcion: "Ajuste/NC ene 2026",
|
||||
Tipo: "egreso", Descripcion: "Ajuste/NC ene 2026",
|
||||
Valor: 130000, CuentaID: cuentaIng, Estado: "registrada",
|
||||
})
|
||||
}
|
||||
@@ -369,7 +373,11 @@ func SeedBalanceData() {
|
||||
}
|
||||
|
||||
// ─── IVA → Transacciones (egresos) ──────────────────────────────────────
|
||||
ivas := []struct{ valor float64; entidad string; desc string }{
|
||||
ivas := []struct {
|
||||
valor float64
|
||||
entidad string
|
||||
desc string
|
||||
}{
|
||||
{285000, "DOCUXER", "IVA Fact #63"},
|
||||
{503500, "DOCUXER", "IVA Fact #70"},
|
||||
{19000, "GIAF SAS", "IVA Fact #56"},
|
||||
@@ -383,7 +391,7 @@ func SeedBalanceData() {
|
||||
eid := getEntidad(iv.entidad)
|
||||
db.Create(&Transaccion{
|
||||
Fecha: parseDate("2026-01-01 00:00:00"),
|
||||
Tipo: "egreso", Descripcion: iv.desc,
|
||||
Tipo: "egreso", Descripcion: iv.desc,
|
||||
Valor: iv.valor, CuentaID: cuentaImp,
|
||||
EntidadID: eid, Estado: "registrada",
|
||||
})
|
||||
@@ -391,7 +399,11 @@ func SeedBalanceData() {
|
||||
}
|
||||
|
||||
// ─── RETENCION → Transacciones (egresos) ────────────────────────────────
|
||||
rets := []struct{ valor float64; fecha string; estado string }{
|
||||
rets := []struct {
|
||||
valor float64
|
||||
fecha string
|
||||
estado string
|
||||
}{
|
||||
{195000, "2026-03-01 00:00:00", "pagado"},
|
||||
{257000, "2026-02-01 00:00:00", "pagado"},
|
||||
{431000, "2026-01-01 00:00:00", "pendiente"},
|
||||
@@ -402,7 +414,7 @@ func SeedBalanceData() {
|
||||
if db.Where("descripcion = ?", desc).First(&existing).Error != nil {
|
||||
db.Create(&Transaccion{
|
||||
Fecha: parseDate(r.fecha),
|
||||
Tipo: "egreso", Descripcion: desc,
|
||||
Tipo: "egreso", Descripcion: desc,
|
||||
Valor: r.valor, CuentaID: cuentaImp,
|
||||
Estado: "registrada",
|
||||
})
|
||||
@@ -410,7 +422,11 @@ func SeedBalanceData() {
|
||||
}
|
||||
|
||||
// ─── CUENTAS DE COBRO (las que te pasan a ti) → CuentasPagar ────────────
|
||||
cobros := []struct{ entidad string; valor float64; fecha string }{
|
||||
cobros := []struct {
|
||||
entidad string
|
||||
valor float64
|
||||
fecha string
|
||||
}{
|
||||
{"NATALIA", 2000000, "2026-01-01 00:00:00"},
|
||||
{"FELIPE", 2000000, "2026-01-01 00:00:00"},
|
||||
{"CONTADORA", 1780000, "2026-01-01 00:00:00"},
|
||||
@@ -439,7 +455,10 @@ func SeedBalanceData() {
|
||||
}
|
||||
|
||||
// ─── CONSOLIDADO POR MES ───────────────────────────────────────────────
|
||||
consols := []struct{ mes int; ing, egre, resul float64 }{
|
||||
consols := []struct {
|
||||
mes int
|
||||
ing, egre, resul float64
|
||||
}{
|
||||
{1, 12669000, 8467849, 4201151},
|
||||
{2, 8470500, 4300000, 4170500},
|
||||
{3, 2109000, 4300000, -2191000},
|
||||
@@ -459,6 +478,14 @@ func SeedBalanceData() {
|
||||
log.Println("[SEED] Balance data imported from BALANCE.numbers")
|
||||
}
|
||||
|
||||
func GetCuentaCobroByID(id uint) (*CuentaCobro, error) {
|
||||
var item CuentaCobro
|
||||
if err := app.Http.Database.DB.Preload("Cliente").Preload("Entidad").First(&item, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func CreateCuentaCobro(cc *CuentaCobro) error {
|
||||
return app.Http.Database.DB.Create(cc).Error
|
||||
}
|
||||
@@ -565,12 +592,12 @@ func CalcularYGuardarConsolidado(mes, anio int) (*ConsolidadoMensual, error) {
|
||||
resultado := ingresos.Total - egresos.Total
|
||||
|
||||
c := &ConsolidadoMensual{
|
||||
Anio: anio,
|
||||
Mes: mes,
|
||||
TotalIngresos: ingresos.Total,
|
||||
TotalEgresos: egresos.Total,
|
||||
Anio: anio,
|
||||
Mes: mes,
|
||||
TotalIngresos: ingresos.Total,
|
||||
TotalEgresos: egresos.Total,
|
||||
TotalRetenciones: 0,
|
||||
Resultado: resultado,
|
||||
Resultado: resultado,
|
||||
}
|
||||
|
||||
var existing ConsolidadoMensual
|
||||
@@ -605,15 +632,15 @@ func ListConsolidados(anio int) ([]ConsolidadoMensual, error) {
|
||||
// ─── Datos para dashboard ────────────────────────────────────────────────────
|
||||
|
||||
type DashboardData struct {
|
||||
Mes int `json:"mes"`
|
||||
Anio int `json:"anio"`
|
||||
TotalIngresos float64 `json:"total_ingresos"`
|
||||
TotalEgresos float64 `json:"total_egresos"`
|
||||
Resultado float64 `json:"resultado"`
|
||||
CantTransacciones int64 `json:"cant_transacciones"`
|
||||
PendientesCobro float64 `json:"pendientes_cobro"`
|
||||
PendientesPago float64 `json:"pendientes_pago"`
|
||||
Transacciones []Transaccion `json:"transacciones"`
|
||||
Mes int `json:"mes"`
|
||||
Anio int `json:"anio"`
|
||||
TotalIngresos float64 `json:"total_ingresos"`
|
||||
TotalEgresos float64 `json:"total_egresos"`
|
||||
Resultado float64 `json:"resultado"`
|
||||
CantTransacciones int64 `json:"cant_transacciones"`
|
||||
PendientesCobro float64 `json:"pendientes_cobro"`
|
||||
PendientesPago float64 `json:"pendientes_pago"`
|
||||
Transacciones []Transaccion `json:"transacciones"`
|
||||
}
|
||||
|
||||
func GetDashboardData(mes, anio int) (*DashboardData, error) {
|
||||
@@ -657,15 +684,15 @@ func GetDashboardData(mes, anio int) (*DashboardData, error) {
|
||||
|
||||
func SeedContabilidad() {
|
||||
cuentas := []Cuenta{
|
||||
{Codigo: "ING-FAC", Nombre: "Facturación", Tipo: "ingreso", Color: "#22c55e"},
|
||||
{Codigo: "ING-OTR", Nombre: "Otros ingresos", Tipo: "ingreso", Color: "#16a34a"},
|
||||
{Codigo: "EGR-HOS", Nombre: "Hosting/Servidores", Tipo: "egreso", Color: "#ef4444"},
|
||||
{Codigo: "EGR-DOM", Nombre: "Dominios", Tipo: "egreso", Color: "#dc2626"},
|
||||
{Codigo: "EGR-SRV", Nombre: "Servicios", Tipo: "egreso", Color: "#f97316"},
|
||||
{Codigo: "ING-FAC", Nombre: "Facturación", Tipo: "ingreso", Color: "#22c55e"},
|
||||
{Codigo: "ING-OTR", Nombre: "Otros ingresos", Tipo: "ingreso", Color: "#16a34a"},
|
||||
{Codigo: "EGR-HOS", Nombre: "Hosting/Servidores", Tipo: "egreso", Color: "#ef4444"},
|
||||
{Codigo: "EGR-DOM", Nombre: "Dominios", Tipo: "egreso", Color: "#dc2626"},
|
||||
{Codigo: "EGR-SRV", Nombre: "Servicios", Tipo: "egreso", Color: "#f97316"},
|
||||
{Codigo: "EGR-GRAL", Nombre: "Gastos generales", Tipo: "egreso", Color: "#eab308"},
|
||||
{Codigo: "EGR-NOM", Nombre: "Nómina", Tipo: "egreso", Color: "#a855f7"},
|
||||
{Codigo: "EGR-IMPU", Nombre: "Impuestos", Tipo: "egreso", Color: "#6366f1"},
|
||||
{Codigo: "EGR-MKT", Nombre: "Marketing", Tipo: "egreso", Color: "#ec4899"},
|
||||
{Codigo: "EGR-NOM", Nombre: "Nómina", Tipo: "egreso", Color: "#a855f7"},
|
||||
{Codigo: "EGR-IMPU", Nombre: "Impuestos", Tipo: "egreso", Color: "#6366f1"},
|
||||
{Codigo: "EGR-MKT", Nombre: "Marketing", Tipo: "egreso", Color: "#ec4899"},
|
||||
}
|
||||
for _, c := range cuentas {
|
||||
var existing Cuenta
|
||||
@@ -675,13 +702,13 @@ func SeedContabilidad() {
|
||||
}
|
||||
|
||||
entidades := []Entidad{
|
||||
{Nombre: "DOCUXER", Tipo: "cliente"},
|
||||
{Nombre: "GIAF SAS", Tipo: "cliente"},
|
||||
{Nombre: "TECZONE", Tipo: "proveedor"},
|
||||
{Nombre: "FELIPE", Tipo: "proveedor"},
|
||||
{Nombre: "NATALIA", Tipo: "proveedor"},
|
||||
{Nombre: "CONTADORA", Tipo: "proveedor"},
|
||||
{Nombre: "ANDREMER", Tipo: "proveedor"},
|
||||
{Nombre: "DOCUXER", Tipo: "cliente"},
|
||||
{Nombre: "GIAF SAS", Tipo: "cliente"},
|
||||
{Nombre: "TECZONE", Tipo: "proveedor"},
|
||||
{Nombre: "FELIPE", Tipo: "proveedor"},
|
||||
{Nombre: "NATALIA", Tipo: "proveedor"},
|
||||
{Nombre: "CONTADORA", Tipo: "proveedor"},
|
||||
{Nombre: "ANDREMER", Tipo: "proveedor"},
|
||||
}
|
||||
for _, e := range entidades {
|
||||
var existing Entidad
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// DocumentoGenerado es el historial de documentos producidos por el motor de
|
||||
// automatización con IA (cotización, contrato, acta/arquitectura, cuenta de cobro),
|
||||
// sin importar el canal que los originó (Telegram, chat propio o Claude directo).
|
||||
type DocumentoGenerado struct {
|
||||
gorm.Model
|
||||
Tipo string `json:"tipo" gorm:"column:tipo;size:30;not null;index"` // cotizacion | contrato | acta | cuenta_cobro
|
||||
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"`
|
||||
Cliente *Cliente `json:"cliente,omitempty" gorm:"foreignKey:ClienteID"`
|
||||
ProyectoID *uint `json:"proyecto_id" gorm:"column:proyecto_id;index"`
|
||||
PlantillaID *uint `json:"plantilla_id" gorm:"column:plantilla_id"`
|
||||
Nombre string `json:"nombre" gorm:"column:nombre;size:200"`
|
||||
Archivo string `json:"archivo" gorm:"column:archivo"` // ruta en disco, ej: uploads/documentos/cotizacion/12/xyz.pdf
|
||||
TipoMime string `json:"tipo_mime" gorm:"column:tipo_mime;default:'application/pdf'"`
|
||||
Tamanio int64 `json:"tamanio" gorm:"column:tamanio"`
|
||||
DatosJSON string `json:"datos_json" gorm:"column:datos_json;type:text"` // input usado para generarlo (auditoría)
|
||||
GeneradoPor string `json:"generado_por" gorm:"column:generado_por;size:20;default:'web'"` // web | telegram | claude_api
|
||||
}
|
||||
|
||||
func (DocumentoGenerado) TableName() string { return "documentos_generados" }
|
||||
|
||||
func CreateDocumentoGenerado(d *DocumentoGenerado) error {
|
||||
return app.Http.Database.DB.Create(d).Error
|
||||
}
|
||||
|
||||
func GetAllDocumentosGenerados(limit, offset int, tipo string, clienteID uint) ([]DocumentoGenerado, int64, error) {
|
||||
var items []DocumentoGenerado
|
||||
var total int64
|
||||
db := app.Http.Database.DB.Model(&DocumentoGenerado{}).Preload("Cliente")
|
||||
if tipo != "" {
|
||||
db = db.Where("tipo = ?", tipo)
|
||||
}
|
||||
if clienteID != 0 {
|
||||
db = db.Where("cliente_id = ?", clienteID)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func GetDocumentoGeneradoByID(id uint) (*DocumentoGenerado, error) {
|
||||
var item DocumentoGenerado
|
||||
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
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).
|
||||
func GetPlantillaDocumentoActiva(tipo string) (*PlantillaDocumento, error) {
|
||||
var item PlantillaDocumento
|
||||
if err := app.Http.Database.DB.
|
||||
Where("tipo = ? AND activa = ?", tipo, true).
|
||||
Order("version DESC").
|
||||
First(&item).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tarifa es la tabla de precios/reglas de negocio que la IA usa para calcular
|
||||
// cotizaciones y contratos: valor por hora según tipo de servicio, licencias M365,
|
||||
// tipos de VM Azure recurrentes, márgenes estándar, etc.
|
||||
type Tarifa struct {
|
||||
gorm.Model
|
||||
Categoria string `json:"categoria" gorm:"column:categoria;size:50;not null;index"` // hora_servicio | licencia | vm_azure | margen | otro
|
||||
Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"`
|
||||
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
||||
Moneda string `json:"moneda" gorm:"column:moneda;size:10;default:'COP'"`
|
||||
Unidad string `json:"unidad" gorm:"column:unidad;size:20"` // hora | mes | unico | porcentaje
|
||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||
}
|
||||
|
||||
func (Tarifa) TableName() string { return "tarifas" }
|
||||
|
||||
func GetAllTarifas(limit, offset int, search, categoria string) ([]Tarifa, int64, error) {
|
||||
var items []Tarifa
|
||||
var total int64
|
||||
db := app.Http.Database.DB.Model(&Tarifa{})
|
||||
if search != "" {
|
||||
db = db.Where("nombre ILIKE ?", "%"+search+"%")
|
||||
}
|
||||
if categoria != "" {
|
||||
db = db.Where("categoria = ?", categoria)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := db.Order("categoria ASC, nombre ASC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
// GetTarifasActivas retorna todas las tarifas activas, opcionalmente filtradas por categoría.
|
||||
// Es lo que usan los endpoints de generación para calcular precios.
|
||||
func GetTarifasActivas(categoria string) ([]Tarifa, error) {
|
||||
var items []Tarifa
|
||||
db := app.Http.Database.DB.Where("activo = ?", true)
|
||||
if categoria != "" {
|
||||
db = db.Where("categoria = ?", categoria)
|
||||
}
|
||||
if err := db.Order("nombre ASC").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func GetTarifaByID(id uint) (*Tarifa, error) {
|
||||
var item Tarifa
|
||||
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func CreateTarifa(t Tarifa) error {
|
||||
return app.Http.Database.DB.Create(&t).Error
|
||||
}
|
||||
|
||||
func UpdateTarifa(id uint, updates map[string]interface{}) error {
|
||||
return app.Http.Database.DB.Model(&Tarifa{}).Where("id = ?", id).Updates(updates).Error
|
||||
}
|
||||
|
||||
func DeleteTarifa(id uint) error {
|
||||
return app.Http.Database.DB.Delete(&Tarifa{}, id).Error
|
||||
}
|
||||
Reference in New Issue
Block a user