Files
soft_usite/rest/controllers/contabilidad_controller.go
T
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

643 lines
19 KiB
Go

package controllers
import (
"math"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// ─── Render de vistas ────────────────────────────────────────────────────────
func ContabilidadIndex(c *fiber.Ctx) error {
return c.Render("contabilidad/contabilidad", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func ContabilidadTransaccionesView(c *fiber.Ctx) error {
return c.Render("contabilidad/transacciones", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func ContabilidadCuentasView(c *fiber.Ctx) error {
return c.Render("contabilidad/cuentas", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func ContabilidadEntidadesView(c *fiber.Ctx) error {
return c.Render("contabilidad/entidades", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func ContabilidadCobroView(c *fiber.Ctx) error {
return c.Render("contabilidad/cuentas_cobro", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func ContabilidadPagarView(c *fiber.Ctx) error {
return c.Render("contabilidad/cuentas_pagar", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
// ─── Dashboard ───────────────────────────────────────────────────────────────
func ContabilidadDashboard(c *fiber.Ctx) error {
now := time.Now()
mes, _ := strconv.Atoi(c.Query("mes", strconv.Itoa(int(now.Month()))))
anio, _ := strconv.Atoi(c.Query("anio", strconv.Itoa(now.Year())))
if mes < 1 || mes > 12 {
mes = int(now.Month())
}
if anio < 2000 {
anio = now.Year()
}
data, err := models.GetDashboardData(mes, anio)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(data)
}
func ContabilidadConsolidado(ctx *fiber.Ctx) error {
now := time.Now()
mes, _ := strconv.Atoi(ctx.Query("mes", strconv.Itoa(int(now.Month()))))
anio, _ := strconv.Atoi(ctx.Query("anio", strconv.Itoa(now.Year())))
if mes < 1 || mes > 12 {
mes = int(now.Month())
}
if anio < 2000 {
anio = now.Year()
}
data, err := models.CalcularYGuardarConsolidado(mes, anio)
if err != nil {
return ctx.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return ctx.JSON(data)
}
func ContabilidadListConsolidados(c *fiber.Ctx) error {
anio, _ := strconv.Atoi(c.Query("anio", "0"))
items, err := models.ListConsolidados(anio)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(items)
}
// =============================================================================
// ─── CRUD: Cuentas ──────────────────────────────────────────────────────────
// =============================================================================
func GetCuentas(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
search := c.Query("search", "")
if page < 1 {
page = 1
}
limit := 50
offset := (page - 1) * limit
items, total, err := models.GetAllCuentas(limit, offset, search)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"items": items,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
func GetCuentasSelect(c *fiber.Ctx) error {
items, err := models.GetAllCuentasSelect()
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(items)
}
func CreateCuenta(c *fiber.Ctx) error {
var req models.Cuenta
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if req.Nombre == "" {
return c.Status(400).JSON(fiber.Map{"error": "nombre es requerido"})
}
if req.Tipo == "" {
req.Tipo = "egreso"
}
req.Activo = true
if err := models.CreateCuenta(&req); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(req)
}
func UpdateCuenta(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
var req models.Cuenta
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
req.ID = uint(id)
if err := models.UpdateCuenta(&req); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func DeleteCuenta(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
if err := models.DeleteCuenta(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
// =============================================================================
// ─── CRUD: Entidades ────────────────────────────────────────────────────────
// =============================================================================
func GetEntidades(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
search := c.Query("search", "")
if page < 1 {
page = 1
}
limit := 20
offset := (page - 1) * limit
items, total, err := models.GetAllEntidades(limit, offset, search)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"items": items,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
func GetEntidadesSelect(c *fiber.Ctx) error {
items, err := models.GetAllEntidadesSelect()
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(items)
}
func CreateEntidad(c *fiber.Ctx) error {
var req models.Entidad
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if req.Nombre == "" {
return c.Status(400).JSON(fiber.Map{"error": "nombre es requerido"})
}
req.Activo = true
if err := models.CreateEntidad(&req); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(req)
}
func UpdateEntidad(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
var req models.Entidad
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
req.ID = uint(id)
if err := models.UpdateEntidad(&req); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func DeleteEntidad(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
if err := models.DeleteEntidad(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
// =============================================================================
// ─── CRUD: Transacciones ────────────────────────────────────────────────────
// =============================================================================
func GetTransacciones(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
search := c.Query("search", "")
filtroTipo := c.Query("tipo", "")
mes, _ := strconv.Atoi(c.Query("mes", "0"))
anio, _ := strconv.Atoi(c.Query("anio", "0"))
if page < 1 {
page = 1
}
limit := 30
offset := (page - 1) * limit
items, total, err := models.GetAllTransacciones(limit, offset, search, filtroTipo, mes, anio)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"items": items,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
func CreateTransaccion(c *fiber.Ctx) error {
type Req struct {
Fecha string `json:"fecha"`
Tipo string `json:"tipo"`
Descripcion string `json:"descripcion"`
Valor float64 `json:"valor"`
CuentaID *uint `json:"cuenta_id"`
EntidadID *uint `json:"entidad_id"`
FormaPago string `json:"forma_pago"`
Estado string `json:"estado"`
Notas string `json:"notas"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if req.Descripcion == "" {
return c.Status(400).JSON(fiber.Map{"error": "descripcion es requerida"})
}
if req.Valor <= 0 {
return c.Status(400).JSON(fiber.Map{"error": "valor debe ser mayor a 0"})
}
t := &models.Transaccion{
Tipo: req.Tipo,
Descripcion: req.Descripcion,
Valor: req.Valor,
CuentaID: req.CuentaID,
EntidadID: req.EntidadID,
FormaPago: req.FormaPago,
Estado: req.Estado,
Notas: req.Notas,
Fecha: time.Now(),
}
if t.Tipo == "" {
t.Tipo = "ingreso"
}
if t.Estado == "" {
t.Estado = "registrada"
}
if req.Fecha != "" {
if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil {
t.Fecha = parsed
}
}
if err := models.CreateTransaccion(t); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(t)
}
func UpdateTransaccion(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
type Req struct {
Fecha string `json:"fecha"`
Tipo string `json:"tipo"`
Descripcion string `json:"descripcion"`
Valor float64 `json:"valor"`
CuentaID *uint `json:"cuenta_id"`
EntidadID *uint `json:"entidad_id"`
FormaPago string `json:"forma_pago"`
Estado string `json:"estado"`
Notas string `json:"notas"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
t := &models.Transaccion{
Tipo: req.Tipo,
Descripcion: req.Descripcion,
Valor: req.Valor,
CuentaID: req.CuentaID,
EntidadID: req.EntidadID,
FormaPago: req.FormaPago,
Estado: req.Estado,
Notas: req.Notas,
Fecha: time.Now(),
}
t.ID = uint(id)
if req.Fecha != "" {
if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil {
t.Fecha = parsed
}
}
if err := models.UpdateTransaccion(t); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func DeleteTransaccion(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
if err := models.DeleteTransaccion(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
// =============================================================================
// ─── CRUD: Cuentas por Cobrar ──────────────────────────────────────────────
// =============================================================================
func GetCuentasCobro(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
search := c.Query("search", "")
estado := c.Query("estado", "")
if page < 1 {
page = 1
}
limit := 20
offset := (page - 1) * limit
items, total, err := models.GetAllCuentasCobro(limit, offset, search, estado)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"items": items,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
func CreateCuentaCobro(c *fiber.Ctx) error {
type Req struct {
ClienteID uint `json:"cliente_id"`
Fecha string `json:"fecha"`
Descripcion string `json:"descripcion"`
Valor float64 `json:"valor"`
FechaVencimiento string `json:"fecha_vencimiento"`
Notas string `json:"notas"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if req.ClienteID == 0 {
return c.Status(400).JSON(fiber.Map{"error": "cliente_id es requerido"})
}
cc := &models.CuentaCobro{
ClienteID: req.ClienteID,
Descripcion: req.Descripcion,
Valor: req.Valor,
Estado: "pendiente",
Notas: req.Notas,
Fecha: time.Now(),
}
if req.Fecha != "" {
if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil {
cc.Fecha = parsed
}
}
if req.FechaVencimiento != "" {
if parsed, err := time.Parse("2006-01-02", req.FechaVencimiento); err == nil {
cc.FechaVencimiento = &parsed
}
}
if err := models.CreateCuentaCobro(cc); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(cc)
}
func UpdateCuentaCobro(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
type Req struct {
Estado string `json:"estado"`
FechaPago string `json:"fecha_pago"`
TransaccionID *uint `json:"transaccion_id"`
Notas string `json:"notas"`
FechaVencimiento string `json:"fecha_vencimiento"`
Descripcion string `json:"descripcion"`
Valor float64 `json:"valor"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
cc := &models.CuentaCobro{}
cc.ID = uint(id)
if req.Estado != "" {
cc.Estado = req.Estado
}
if req.FechaPago != "" {
if parsed, err := time.Parse("2006-01-02", req.FechaPago); err == nil {
cc.FechaPago = &parsed
}
}
cc.TransaccionID = req.TransaccionID
cc.Notas = req.Notas
if err := models.UpdateCuentaCobro(cc); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func DeleteCuentaCobro(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
if err := models.DeleteCuentaCobro(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
// =============================================================================
// ─── CRUD: Cuentas por Pagar ────────────────────────────────────────────────
// =============================================================================
func GetCuentasPagar(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
search := c.Query("search", "")
estado := c.Query("estado", "")
if page < 1 {
page = 1
}
limit := 20
offset := (page - 1) * limit
items, total, err := models.GetAllCuentasPagar(limit, offset, search, estado)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"items": items,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
func CreateCuentaPagar(c *fiber.Ctx) error {
type Req struct {
EntidadID uint `json:"entidad_id"`
Fecha string `json:"fecha"`
Descripcion string `json:"descripcion"`
Valor float64 `json:"valor"`
Vencimiento string `json:"vencimiento"`
Notas string `json:"notas"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if req.EntidadID == 0 {
return c.Status(400).JSON(fiber.Map{"error": "entidad_id es requerido"})
}
cp := &models.CuentaPagar{
EntidadID: req.EntidadID,
Descripcion: req.Descripcion,
Valor: req.Valor,
Estado: "pendiente",
Notas: req.Notas,
Fecha: time.Now(),
}
if req.Fecha != "" {
if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil {
cp.Fecha = parsed
}
}
if req.Vencimiento != "" {
if parsed, err := time.Parse("2006-01-02", req.Vencimiento); err == nil {
cp.Vencimiento = &parsed
}
}
if err := models.CreateCuentaPagar(cp); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(cp)
}
func UpdateCuentaPagar(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
type Req struct {
Estado string `json:"estado"`
FechaPago string `json:"fecha_pago"`
TransaccionID *uint `json:"transaccion_id"`
Notas string `json:"notas"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
cp := &models.CuentaPagar{}
cp.ID = uint(id)
if req.Estado != "" {
cp.Estado = req.Estado
}
if req.FechaPago != "" {
if parsed, err := time.Parse("2006-01-02", req.FechaPago); err == nil {
cp.FechaPago = &parsed
}
}
cp.TransaccionID = req.TransaccionID
cp.Notas = req.Notas
if err := models.UpdateCuentaPagar(cp); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
func MarcarCuentaPagarPagada(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
var req struct {
FechaPago string `json:"fecha_pago"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
fecha := time.Now()
if req.FechaPago != "" {
if parsed, err := time.Parse("2006-01-02", req.FechaPago); err == nil {
fecha = parsed
}
}
if err := models.MarcarCuentaPagarPagada(uint(id), fecha); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
// GenerarDocumentoCuentaCobro produce el PDF de solicitud de cuenta de cobro a
// partir de la plantilla activa tipo 'cuenta_cobro'.
// POST /api/v2/contabilidad/cuentas-cobro/:id/generar-documento
func GenerarDocumentoCuentaCobro(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
doc, err := services.GenerarDocumentoCuentaCobro(uint(id), generadoPorFromContext(c))
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"ok": true, "documento": doc})
}
func DeleteCuentaPagar(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
if err := models.DeleteCuentaPagar(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}