- El agente creaba tareas con estado='pendiente' (default), pero el tablero Kanban del dashboard solo reconoce por_hacer/en_progreso/revision/hecho. La tarea se guardaba bien (por eso llegaba el correo de notificación) pero no aparecía en ninguna columna. Se corrigen los enums y el default de las tools crear_tarea/actualizar_estado_tarea, y se agrega una reparación única al arranque que corrige las tareas ya creadas con el estado inválido. - adjuntar_factura siempre asumía factura de VENTA (ligada a un cliente). Se agrega adjuntar_factura_compra: cuando un proveedor le factura a U-SITE (no al revés), busca/crea la Entidad proveedor y registra una cuenta por pagar con el documento adjunto (se agregan campos archivo/original_name/tipo_mime a CuentaPagar, que no los tenía). El prompt del sistema instruye a Claude a decidir la dirección leyendo quién emite y quién recibe el documento. De paso: endpoint de descarga del soporte de la factura de compra, expuesto también en el dashboard (/app/contabilidad/cuentas-pagar). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
852 lines
31 KiB
Go
852 lines
31 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ─── Cuenta (plan de cuentas / categoría contable) ───────────────────────────
|
|
|
|
type Cuenta struct {
|
|
gorm.Model
|
|
Codigo string `json:"codigo" gorm:"column:codigo;size:20"`
|
|
Nombre string `json:"nombre" gorm:"column:nombre;size:255;not null"`
|
|
Tipo string `json:"tipo" gorm:"column:tipo;size:20;not null"` // ingreso | egreso
|
|
Color string `json:"color" gorm:"column:color;size:7"`
|
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
|
}
|
|
|
|
func (Cuenta) TableName() string { return "contab_cuentas" }
|
|
|
|
// ─── Entidad (cliente, proveedor, empresa, persona) ──────────────────────────
|
|
|
|
type Entidad struct {
|
|
gorm.Model
|
|
Nombre string `json:"nombre" gorm:"column:nombre;size:255;not null"`
|
|
Tipo string `json:"tipo" gorm:"column:tipo;size:20"` // cliente | proveedor | ambos
|
|
Documento string `json:"documento" gorm:"column:documento;size:50"`
|
|
Email string `json:"email" gorm:"column:email;size:255"`
|
|
Telefono string `json:"telefono" gorm:"column:telefono;size:50"`
|
|
Contacto string `json:"contacto" gorm:"column:contacto;size:255"`
|
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
|
}
|
|
|
|
func (Entidad) TableName() string { return "contab_entidades" }
|
|
|
|
// ─── Transaccion (ingreso / egreso) ──────────────────────────────────────────
|
|
|
|
type Transaccion struct {
|
|
gorm.Model
|
|
Fecha time.Time `json:"fecha" gorm:"column:fecha;not null"`
|
|
Tipo string `json:"tipo" gorm:"column:tipo;size:10;not null"` // ingreso | egreso
|
|
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
|
|
Valor float64 `json:"valor" gorm:"column:valor;not null"`
|
|
CuentaID *uint `json:"cuenta_id" gorm:"column:cuenta_id;index"`
|
|
Cuenta *Cuenta `json:"cuenta" gorm:"foreignKey:CuentaID"`
|
|
EntidadID *uint `json:"entidad_id" gorm:"column:entidad_id;index"`
|
|
Entidad *Entidad `json:"entidad" gorm:"foreignKey:EntidadID"`
|
|
FormaPago string `json:"forma_pago" gorm:"column:forma_pago;size:30"` // transferencia | efectivo | tarjeta | cheque | otro
|
|
Estado string `json:"estado" gorm:"column:estado;size:20;default:'registrada'"`
|
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
|
}
|
|
|
|
func (Transaccion) TableName() string { return "contab_transacciones" }
|
|
|
|
// ─── CuentaCobro (cuentas por cobrar) ────────────────────────────────────────
|
|
|
|
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"`
|
|
}
|
|
|
|
func (CuentaCobro) TableName() string { return "contab_cuentas_cobro" }
|
|
|
|
// ─── CuentaPagar (cuentas por pagar) ─────────────────────────────────────────
|
|
|
|
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"`
|
|
// Soporte de la factura de compra (el documento del proveedor), igual que
|
|
// Factura.Archivo para las facturas de venta.
|
|
Archivo string `json:"archivo" gorm:"column:archivo"`
|
|
OriginalName string `json:"original_name" gorm:"column:original_name"`
|
|
TipoMime string `json:"tipo_mime" gorm:"column:tipo_mime"`
|
|
}
|
|
|
|
func (CuentaPagar) TableName() string { return "contab_cuentas_pagar" }
|
|
|
|
// ─── ConsolidadoMensual ──────────────────────────────────────────────────────
|
|
|
|
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"`
|
|
TotalRetenciones float64 `json:"total_retenciones" gorm:"column:total_retenciones;default:0"`
|
|
Resultado float64 `json:"resultado" gorm:"column:resultado;default:0"`
|
|
}
|
|
|
|
func (ConsolidadoMensual) TableName() string { return "contab_consolidado" }
|
|
|
|
// =============================================================================
|
|
// ─── CRUD: Cuenta ───────────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
func GetAllCuentas(limit, offset int, search string) ([]Cuenta, int64, error) {
|
|
var items []Cuenta
|
|
var total int64
|
|
db := app.Http.Database.DB.Model(&Cuenta{})
|
|
if search != "" {
|
|
db = db.Where("nombre ILIKE ? OR codigo ILIKE ?", "%"+search+"%", "%"+search+"%")
|
|
}
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := db.Order("tipo ASC, nombre ASC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func GetAllCuentasSelect() ([]Cuenta, error) {
|
|
var items []Cuenta
|
|
if err := app.Http.Database.DB.Where("activo = ?", true).Order("nombre ASC").Find(&items).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func CreateCuenta(c *Cuenta) error {
|
|
return app.Http.Database.DB.Create(c).Error
|
|
}
|
|
|
|
func UpdateCuenta(c *Cuenta) error {
|
|
return app.Http.Database.DB.Model(&Cuenta{}).Where("id = ?", c.ID).Updates(map[string]interface{}{
|
|
"codigo": c.Codigo,
|
|
"nombre": c.Nombre,
|
|
"tipo": c.Tipo,
|
|
"color": c.Color,
|
|
"activo": c.Activo,
|
|
}).Error
|
|
}
|
|
|
|
func DeleteCuenta(id uint) error {
|
|
return app.Http.Database.DB.Delete(&Cuenta{}, id).Error
|
|
}
|
|
|
|
// =============================================================================
|
|
// ─── CRUD: Entidad ──────────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
func GetAllEntidades(limit, offset int, search string) ([]Entidad, int64, error) {
|
|
var items []Entidad
|
|
var total int64
|
|
db := app.Http.Database.DB.Model(&Entidad{})
|
|
if search != "" {
|
|
db = db.Where("nombre ILIKE ? OR documento ILIKE ? OR email ILIKE ?",
|
|
"%"+search+"%", "%"+search+"%", "%"+search+"%")
|
|
}
|
|
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 GetAllEntidadesSelect() ([]Entidad, error) {
|
|
var items []Entidad
|
|
if err := app.Http.Database.DB.Where("activo = ?", true).Order("nombre ASC").Find(&items).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func CreateEntidad(e *Entidad) error {
|
|
return app.Http.Database.DB.Create(e).Error
|
|
}
|
|
|
|
// GetOrCreateEntidadProveedor busca un proveedor por nombre (sin distinguir
|
|
// mayúsculas) y lo crea si no existe. Se usa para registrar facturas de compra
|
|
// sin obligar a dar de alta al proveedor manualmente primero.
|
|
func GetOrCreateEntidadProveedor(nombre string) (*Entidad, error) {
|
|
nombre = strings.TrimSpace(nombre)
|
|
if nombre == "" {
|
|
return nil, fmt.Errorf("nombre de proveedor requerido")
|
|
}
|
|
var e Entidad
|
|
err := app.Http.Database.DB.Where("LOWER(nombre) = LOWER(?)", nombre).First(&e).Error
|
|
if err == nil {
|
|
return &e, nil
|
|
}
|
|
e = Entidad{Nombre: nombre, Tipo: "proveedor", Activo: true}
|
|
if err := app.Http.Database.DB.Create(&e).Error; err != nil {
|
|
return nil, fmt.Errorf("no se pudo crear el proveedor: %w", err)
|
|
}
|
|
return &e, nil
|
|
}
|
|
|
|
func UpdateEntidad(e *Entidad) error {
|
|
return app.Http.Database.DB.Model(&Entidad{}).Where("id = ?", e.ID).Updates(map[string]interface{}{
|
|
"nombre": e.Nombre,
|
|
"tipo": e.Tipo,
|
|
"documento": e.Documento,
|
|
"email": e.Email,
|
|
"telefono": e.Telefono,
|
|
"contacto": e.Contacto,
|
|
"notas": e.Notas,
|
|
"activo": e.Activo,
|
|
}).Error
|
|
}
|
|
|
|
func DeleteEntidad(id uint) error {
|
|
return app.Http.Database.DB.Delete(&Entidad{}, id).Error
|
|
}
|
|
|
|
// =============================================================================
|
|
// ─── CRUD: Transaccion ──────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
func GetAllTransacciones(limit, offset int, search string, filtroTipo string, mes int, anio int) ([]Transaccion, int64, error) {
|
|
var items []Transaccion
|
|
var total int64
|
|
db := app.Http.Database.DB.Model(&Transaccion{}).Preload("Cuenta").Preload("Entidad")
|
|
if search != "" {
|
|
db = db.Where("descripcion ILIKE ?", "%"+search+"%")
|
|
}
|
|
if filtroTipo != "" {
|
|
db = db.Where("tipo = ?", filtroTipo)
|
|
}
|
|
if mes > 0 && anio > 0 {
|
|
db = db.Where("EXTRACT(MONTH FROM fecha) = ? AND EXTRACT(YEAR FROM fecha) = ?", mes, anio)
|
|
}
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := db.Order("fecha DESC, created_at DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func CreateTransaccion(t *Transaccion) error {
|
|
if err := app.Http.Database.DB.Create(t).Error; err != nil {
|
|
return err
|
|
}
|
|
recomputarConsolidadoDeFecha(t.Fecha)
|
|
return nil
|
|
}
|
|
|
|
// UpdateTransaccion actualiza una transacción y recalcula el consolidado mensual
|
|
// del mes al que queda asociada. Si además cambió de mes, también recalcula el
|
|
// mes anterior, para que ninguno de los dos quede desincronizado.
|
|
func UpdateTransaccion(t *Transaccion) error {
|
|
var anterior Transaccion
|
|
tieneAnterior := app.Http.Database.DB.First(&anterior, t.ID).Error == nil
|
|
|
|
if err := app.Http.Database.DB.Model(&Transaccion{}).Where("id = ?", t.ID).Updates(map[string]interface{}{
|
|
"fecha": t.Fecha,
|
|
"tipo": t.Tipo,
|
|
"descripcion": t.Descripcion,
|
|
"valor": t.Valor,
|
|
"cuenta_id": t.CuentaID,
|
|
"entidad_id": t.EntidadID,
|
|
"forma_pago": t.FormaPago,
|
|
"estado": t.Estado,
|
|
"notas": t.Notas,
|
|
}).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
recomputarConsolidadoDeFecha(t.Fecha)
|
|
if tieneAnterior && !mismoMesYAnio(anterior.Fecha, t.Fecha) {
|
|
recomputarConsolidadoDeFecha(anterior.Fecha)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteTransaccion elimina una transacción y recalcula el consolidado mensual
|
|
// del mes al que pertenecía, para que no quede con montos que ya no existen.
|
|
func DeleteTransaccion(id uint) error {
|
|
var t Transaccion
|
|
tieneFecha := app.Http.Database.DB.First(&t, id).Error == nil
|
|
|
|
if err := app.Http.Database.DB.Delete(&Transaccion{}, id).Error; err != nil {
|
|
return err
|
|
}
|
|
if tieneFecha {
|
|
recomputarConsolidadoDeFecha(t.Fecha)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func mismoMesYAnio(a, b time.Time) bool {
|
|
return a.Year() == b.Year() && a.Month() == b.Month()
|
|
}
|
|
|
|
// recomputarConsolidadoDeFecha recalcula (o crea) el ConsolidadoMensual del mes
|
|
// de la fecha dada. Se ignora el error: el consolidado es derivado y se puede
|
|
// recalcular de nuevo en cualquier momento desde /contabilidad/consolidado.
|
|
func recomputarConsolidadoDeFecha(fecha time.Time) {
|
|
_, _ = CalcularYGuardarConsolidado(int(fecha.Month()), fecha.Year())
|
|
}
|
|
|
|
// =============================================================================
|
|
// ─── CRUD: CuentaCobro ──────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
func GetAllCuentasCobro(limit, offset int, search string, estado string) ([]CuentaCobro, int64, error) {
|
|
var items []CuentaCobro
|
|
var total int64
|
|
db := app.Http.Database.DB.Model(&CuentaCobro{}).Preload("Cliente").Preload("Entidad").Preload("Transaccion")
|
|
if search != "" {
|
|
db = db.Joins("JOIN clientes ON clientes.id = contab_cuentas_cobro.cliente_id").
|
|
Where("clientes.nombre ILIKE ? OR clientes.empresa ILIKE ? OR contab_cuentas_cobro.descripcion ILIKE ?",
|
|
"%"+search+"%", "%"+search+"%", "%"+search+"%")
|
|
}
|
|
if estado != "" {
|
|
db = db.Where("contab_cuentas_cobro.estado = ?", estado)
|
|
}
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := db.Order("contab_cuentas_cobro.fecha DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func SeedBalanceData() {
|
|
db := app.Http.Database.DB
|
|
|
|
parseDate := func(s string) time.Time {
|
|
t, _ := time.Parse("2006-01-02 15:04:05", s)
|
|
return t
|
|
}
|
|
|
|
getEntidad := func(nombre string) *uint {
|
|
var e Entidad
|
|
if err := db.Where("nombre = ?", nombre).First(&e).Error; err == nil {
|
|
return &e.ID
|
|
}
|
|
e = Entidad{Nombre: nombre, Tipo: "cliente", Activo: true}
|
|
db.Create(&e)
|
|
return &e.ID
|
|
}
|
|
|
|
getCuenta := func(codigo string) *uint {
|
|
var c Cuenta
|
|
if err := db.Where("codigo = ?", codigo).First(&c).Error; err == nil {
|
|
return &c.ID
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ─── FACTURAS → Transacciones (ingresos) ────────────────────────────────
|
|
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"},
|
|
{57, 330000, "2026-01-01 00:00:00"},
|
|
{58, 1000000, "2026-01-01 00:00:00"},
|
|
{59, 1000000, "2026-01-01 00:00:00"},
|
|
{60, 900000, "2026-01-01 00:00:00"},
|
|
{0, 0, "2026-01-01 00:00:00"}, // ajuste -130000
|
|
{61, 580000, "2026-02-01 00:00:00"},
|
|
{62, 330000, "2026-02-01 00:00:00"},
|
|
{63, 1785000, "2026-02-01 00:00:00"},
|
|
{64, 120000, "2026-02-01 00:00:00"},
|
|
{65, 350000, "2026-02-01 00:00:00"},
|
|
{66, 550000, "2026-02-01 00:00:00"},
|
|
{67, 1300000, "2026-02-01 00:00:00"},
|
|
{68, 270000, "2026-02-01 00:00:00"},
|
|
{69, 32000, "2026-02-01 00:00:00"},
|
|
{70, 3153500, "2026-02-01 00:00:00"},
|
|
{71, 349000, "2026-03-01 00:00:00"},
|
|
{72, 0, "2026-03-01 00:00:00"},
|
|
{73, 1380000, "2026-03-01 00:00:00"},
|
|
{74, 380000, "2026-03-01 00:00:00"},
|
|
{75, 2650000, "2026-04-01 00:00:00"},
|
|
{76, 390000, "2026-04-01 00:00:00"},
|
|
{77, 2200000, "2026-04-01 00:00:00"},
|
|
{78, 4400000, "2026-04-01 00:00:00"},
|
|
{79, 142000, "2026-04-01 00:00:00"},
|
|
}
|
|
cuentaIng := getCuenta("ING-FAC")
|
|
docuxer := getEntidad("DOCUXER")
|
|
giaf := getEntidad("GIAF SAS")
|
|
|
|
for _, f := range facturas {
|
|
if f.factura == 0 && f.valor == 0 {
|
|
// ajuste negativo: nota crédito
|
|
var existing Transaccion
|
|
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",
|
|
Valor: 130000, CuentaID: cuentaIng, Estado: "registrada",
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
desc := fmt.Sprintf("Factura #%.0f", f.factura)
|
|
var existing Transaccion
|
|
if db.Where("descripcion = ? AND fecha = ?", desc, parseDate(f.fecha)).First(&existing).Error != nil {
|
|
eid := docuxer
|
|
if f.factura == 56 {
|
|
eid = giaf
|
|
}
|
|
db.Create(&Transaccion{
|
|
Fecha: parseDate(f.fecha), Tipo: "ingreso",
|
|
Descripcion: desc, Valor: f.valor,
|
|
CuentaID: cuentaIng, EntidadID: eid, Estado: "registrada",
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── IVA → Transacciones (egresos) ──────────────────────────────────────
|
|
ivas := []struct {
|
|
valor float64
|
|
entidad string
|
|
desc string
|
|
}{
|
|
{285000, "DOCUXER", "IVA Fact #63"},
|
|
{503500, "DOCUXER", "IVA Fact #70"},
|
|
{19000, "GIAF SAS", "IVA Fact #56"},
|
|
{503500, "DOCUXER", "IVA Fact #75"},
|
|
{418000, "DOCUXER", "IVA Fact #77"},
|
|
}
|
|
cuentaImp := getCuenta("EGR-IMPU")
|
|
for _, iv := range ivas {
|
|
var existing Transaccion
|
|
if db.Where("descripcion = ?", iv.desc).First(&existing).Error != nil {
|
|
eid := getEntidad(iv.entidad)
|
|
db.Create(&Transaccion{
|
|
Fecha: parseDate("2026-01-01 00:00:00"),
|
|
Tipo: "egreso", Descripcion: iv.desc,
|
|
Valor: iv.valor, CuentaID: cuentaImp,
|
|
EntidadID: eid, Estado: "registrada",
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── RETENCION → Transacciones (egresos) ────────────────────────────────
|
|
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"},
|
|
}
|
|
for _, r := range rets {
|
|
desc := fmt.Sprintf("Retención %s", r.fecha[:7])
|
|
var existing Transaccion
|
|
if db.Where("descripcion = ?", desc).First(&existing).Error != nil {
|
|
db.Create(&Transaccion{
|
|
Fecha: parseDate(r.fecha),
|
|
Tipo: "egreso", Descripcion: desc,
|
|
Valor: r.valor, CuentaID: cuentaImp,
|
|
Estado: "registrada",
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── CUENTAS DE COBRO (las que te pasan a ti) → CuentasPagar ────────────
|
|
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"},
|
|
{"FELIPE", 2300000, "2026-02-01 00:00:00"},
|
|
{"NATALIA", 2000000, "2026-02-01 00:00:00"},
|
|
{"FELIPE", 2300000, "2026-03-01 00:00:00"},
|
|
{"NATALIA", 2000000, "2026-03-01 00:00:00"},
|
|
{"FELIPE", 520000, "2026-04-01 00:00:00"},
|
|
{"ANDREMER", 80000, "2026-04-01 00:00:00"},
|
|
{"NATALIA", 2000000, "2026-04-01 00:00:00"},
|
|
// FAC POR PAGAR
|
|
{"TECZONE", 2687849, "2026-01-01 00:00:00"},
|
|
}
|
|
for _, cb := range cobros {
|
|
eid := getEntidad(cb.entidad)
|
|
desc := fmt.Sprintf("%s %s", cb.entidad, cb.fecha[:7])
|
|
var existing CuentaPagar
|
|
if db.Where("descripcion = ?", desc).First(&existing).Error != nil {
|
|
df := parseDate(cb.fecha)
|
|
db.Create(&CuentaPagar{
|
|
EntidadID: *eid, Fecha: df,
|
|
Descripcion: desc, Valor: cb.valor,
|
|
Estado: "pendiente",
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── CONSOLIDADO POR MES ───────────────────────────────────────────────
|
|
consols := []struct {
|
|
mes int
|
|
ing, egre, resul float64
|
|
}{
|
|
{1, 12669000, 8467849, 4201151},
|
|
{2, 8470500, 4300000, 4170500},
|
|
{3, 2109000, 4300000, -2191000},
|
|
{4, 9782000, 2600000, 7182000},
|
|
}
|
|
for _, cs := range consols {
|
|
var existing ConsolidadoMensual
|
|
if db.Where("anio = 2026 AND mes = ?", cs.mes).First(&existing).Error != nil {
|
|
db.Create(&ConsolidadoMensual{
|
|
Anio: 2026, Mes: cs.mes,
|
|
TotalIngresos: cs.ing, TotalEgresos: cs.egre,
|
|
Resultado: cs.resul,
|
|
})
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// UpdateCuentaCobro actualiza los campos editables de una cuenta por cobrar.
|
|
// No toca transaccion_id a propósito: ese vínculo solo lo debe crear
|
|
// MarcarCuentaCobroPagada, para no perderlo en una edición cualquiera.
|
|
func UpdateCuentaCobro(cc *CuentaCobro) error {
|
|
return app.Http.Database.DB.Model(&CuentaCobro{}).Where("id = ?", cc.ID).Updates(map[string]interface{}{
|
|
"cliente_id": cc.ClienteID,
|
|
"estado": cc.Estado,
|
|
"fecha_vencimiento": cc.FechaVencimiento,
|
|
"fecha_pago": cc.FechaPago,
|
|
"notas": cc.Notas,
|
|
}).Error
|
|
}
|
|
|
|
// MarcarCuentaCobroPagada registra el pago de una cuenta por cobrar: crea la
|
|
// Transaccion de tipo "ingreso" correspondiente y la vincula. Es idempotente —
|
|
// si la cuenta ya estaba pagada, no crea una transacción duplicada.
|
|
func MarcarCuentaCobroPagada(id uint, fechaPago time.Time) error {
|
|
var cc CuentaCobro
|
|
if err := app.Http.Database.DB.Preload("Cliente").First(&cc, id).Error; err != nil {
|
|
return err
|
|
}
|
|
if cc.Estado == "pagado" {
|
|
return nil
|
|
}
|
|
desc := fmt.Sprintf("Pago cuenta por cobrar #%d: %s", cc.ID, cc.Descripcion)
|
|
if cc.Cliente.Nombre != "" {
|
|
desc = fmt.Sprintf("Pago cuenta por cobrar #%d (%s): %s", cc.ID, cc.Cliente.Nombre, cc.Descripcion)
|
|
}
|
|
t := &Transaccion{
|
|
Fecha: fechaPago,
|
|
Tipo: "ingreso",
|
|
Descripcion: desc,
|
|
Valor: cc.Valor,
|
|
}
|
|
if err := CreateTransaccion(t); err != nil {
|
|
return fmt.Errorf("no se pudo crear la transacción de pago: %w", err)
|
|
}
|
|
return app.Http.Database.DB.Model(&CuentaCobro{}).Where("id = ?", id).Updates(map[string]interface{}{
|
|
"estado": "pagado",
|
|
"fecha_pago": fechaPago,
|
|
"transaccion_id": t.ID,
|
|
}).Error
|
|
}
|
|
|
|
func DeleteCuentaCobro(id uint) error {
|
|
return app.Http.Database.DB.Delete(&CuentaCobro{}, id).Error
|
|
}
|
|
|
|
// =============================================================================
|
|
// ─── CRUD: CuentaPagar ──────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
func GetCuentaPagarByID(id uint) (*CuentaPagar, error) {
|
|
var item CuentaPagar
|
|
if err := app.Http.Database.DB.First(&item, id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &item, nil
|
|
}
|
|
|
|
func GetAllCuentasPagar(limit, offset int, search string, estado string) ([]CuentaPagar, int64, error) {
|
|
var items []CuentaPagar
|
|
var total int64
|
|
db := app.Http.Database.DB.Model(&CuentaPagar{}).Preload("Entidad").Preload("Transaccion")
|
|
if search != "" {
|
|
db = db.Joins("JOIN contab_entidades ON contab_entidades.id = contab_cuentas_pagar.entidad_id").
|
|
Where("contab_entidades.nombre ILIKE ? OR contab_cuentas_pagar.descripcion ILIKE ?",
|
|
"%"+search+"%", "%"+search+"%")
|
|
}
|
|
if estado != "" {
|
|
db = db.Where("contab_cuentas_pagar.estado = ?", estado)
|
|
}
|
|
if err := db.Count(&total).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := db.Order("contab_cuentas_pagar.fecha DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func CreateCuentaPagar(cp *CuentaPagar) error {
|
|
return app.Http.Database.DB.Create(cp).Error
|
|
}
|
|
|
|
func UpdateCuentaPagar(cp *CuentaPagar) error {
|
|
return app.Http.Database.DB.Model(&CuentaPagar{}).Where("id = ?", cp.ID).Updates(map[string]interface{}{
|
|
"entidad_id": cp.EntidadID,
|
|
"fecha": cp.Fecha,
|
|
"descripcion": cp.Descripcion,
|
|
"valor": cp.Valor,
|
|
"vencimiento": cp.Vencimiento,
|
|
"estado": cp.Estado,
|
|
"fecha_pago": cp.FechaPago,
|
|
"transaccion_id": cp.TransaccionID,
|
|
"notas": cp.Notas,
|
|
}).Error
|
|
}
|
|
|
|
// MarcarCuentaPagarPagada registra el pago de una cuenta por pagar: crea la
|
|
// Transaccion de tipo "egreso" correspondiente y la vincula. Es idempotente —
|
|
// si la cuenta ya estaba pagada, no crea una transacción duplicada.
|
|
func MarcarCuentaPagarPagada(id uint, fechaPago time.Time) error {
|
|
var cp CuentaPagar
|
|
if err := app.Http.Database.DB.Preload("Entidad").First(&cp, id).Error; err != nil {
|
|
return err
|
|
}
|
|
if cp.Estado == "pagado" {
|
|
return nil
|
|
}
|
|
desc := fmt.Sprintf("Pago cuenta por pagar #%d: %s", cp.ID, cp.Descripcion)
|
|
if cp.Entidad.Nombre != "" {
|
|
desc = fmt.Sprintf("Pago cuenta por pagar #%d (%s): %s", cp.ID, cp.Entidad.Nombre, cp.Descripcion)
|
|
}
|
|
t := &Transaccion{
|
|
Fecha: fechaPago,
|
|
Tipo: "egreso",
|
|
Descripcion: desc,
|
|
Valor: cp.Valor,
|
|
EntidadID: &cp.EntidadID,
|
|
}
|
|
if err := CreateTransaccion(t); err != nil {
|
|
return fmt.Errorf("no se pudo crear la transacción de pago: %w", err)
|
|
}
|
|
return app.Http.Database.DB.Model(&CuentaPagar{}).Where("id = ?", id).Updates(map[string]interface{}{
|
|
"estado": "pagado",
|
|
"fecha_pago": fechaPago,
|
|
"transaccion_id": t.ID,
|
|
}).Error
|
|
}
|
|
|
|
func DeleteCuentaPagar(id uint) error {
|
|
return app.Http.Database.DB.Delete(&CuentaPagar{}, id).Error
|
|
}
|
|
|
|
// =============================================================================
|
|
// ─── Consolidado ────────────────────────────────────────────────────────────
|
|
// =============================================================================
|
|
|
|
func GetConsolidadoMensual(mes, anio int) (*ConsolidadoMensual, error) {
|
|
var c ConsolidadoMensual
|
|
err := app.Http.Database.DB.Where("mes = ? AND anio = ?", mes, anio).First(&c).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func CalcularYGuardarConsolidado(mes, anio int) (*ConsolidadoMensual, error) {
|
|
type SumRow struct {
|
|
Total float64
|
|
}
|
|
var ingresos SumRow
|
|
var egresos SumRow
|
|
|
|
app.Http.Database.DB.Model(&Transaccion{}).
|
|
Select("COALESCE(SUM(valor),0) as total").
|
|
Where("tipo = ? AND EXTRACT(MONTH FROM fecha) = ? AND EXTRACT(YEAR FROM fecha) = ?", "ingreso", mes, anio).
|
|
Scan(&ingresos)
|
|
|
|
app.Http.Database.DB.Model(&Transaccion{}).
|
|
Select("COALESCE(SUM(valor),0) as total").
|
|
Where("tipo = ? AND EXTRACT(MONTH FROM fecha) = ? AND EXTRACT(YEAR FROM fecha) = ?", "egreso", mes, anio).
|
|
Scan(&egresos)
|
|
|
|
resultado := ingresos.Total - egresos.Total
|
|
|
|
c := &ConsolidadoMensual{
|
|
Anio: anio,
|
|
Mes: mes,
|
|
TotalIngresos: ingresos.Total,
|
|
TotalEgresos: egresos.Total,
|
|
TotalRetenciones: 0,
|
|
Resultado: resultado,
|
|
}
|
|
|
|
var existing ConsolidadoMensual
|
|
err := app.Http.Database.DB.Where("mes = ? AND anio = ?", mes, anio).First(&existing).Error
|
|
if err == nil {
|
|
c.ID = existing.ID
|
|
app.Http.Database.DB.Model(&ConsolidadoMensual{}).Where("id = ?", c.ID).Updates(map[string]interface{}{
|
|
"total_ingresos": c.TotalIngresos,
|
|
"total_egresos": c.TotalEgresos,
|
|
"total_retenciones": c.TotalRetenciones,
|
|
"resultado": c.Resultado,
|
|
})
|
|
} else {
|
|
app.Http.Database.DB.Create(c)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
func ListConsolidados(anio int) ([]ConsolidadoMensual, error) {
|
|
var items []ConsolidadoMensual
|
|
db := app.Http.Database.DB.Model(&ConsolidadoMensual{}).Order("anio DESC, mes DESC")
|
|
if anio > 0 {
|
|
db = db.Where("anio = ?", anio)
|
|
}
|
|
if err := db.Find(&items).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
// ─── 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"`
|
|
}
|
|
|
|
func GetDashboardData(mes, anio int) (*DashboardData, error) {
|
|
d := &DashboardData{Mes: mes, Anio: anio}
|
|
|
|
app.Http.Database.DB.Model(&Transaccion{}).
|
|
Select("COALESCE(SUM(valor),0) as total").
|
|
Where("tipo = ? AND EXTRACT(MONTH FROM fecha) = ? AND EXTRACT(YEAR FROM fecha) = ?", "ingreso", mes, anio).
|
|
Scan(&d.TotalIngresos)
|
|
|
|
app.Http.Database.DB.Model(&Transaccion{}).
|
|
Select("COALESCE(SUM(valor),0) as total").
|
|
Where("tipo = ? AND EXTRACT(MONTH FROM fecha) = ? AND EXTRACT(YEAR FROM fecha) = ?", "egreso", mes, anio).
|
|
Scan(&d.TotalEgresos)
|
|
|
|
d.Resultado = d.TotalIngresos - d.TotalEgresos
|
|
|
|
app.Http.Database.DB.Model(&Transaccion{}).
|
|
Where("EXTRACT(MONTH FROM fecha) = ? AND EXTRACT(YEAR FROM fecha) = ?", mes, anio).
|
|
Count(&d.CantTransacciones)
|
|
|
|
app.Http.Database.DB.Model(&CuentaCobro{}).
|
|
Select("COALESCE(SUM(valor),0) as total").
|
|
Where("estado = ?", "pendiente").
|
|
Scan(&d.PendientesCobro)
|
|
|
|
app.Http.Database.DB.Model(&CuentaPagar{}).
|
|
Select("COALESCE(SUM(valor),0) as total").
|
|
Where("estado = ?", "pendiente").
|
|
Scan(&d.PendientesPago)
|
|
|
|
app.Http.Database.DB.Model(&Transaccion{}).
|
|
Preload("Cuenta").Preload("Entidad").
|
|
Where("EXTRACT(MONTH FROM fecha) = ? AND EXTRACT(YEAR FROM fecha) = ?", mes, anio).
|
|
Order("fecha DESC").
|
|
Limit(10).
|
|
Find(&d.Transacciones)
|
|
|
|
return d, nil
|
|
}
|
|
|
|
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: "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"},
|
|
}
|
|
for _, c := range cuentas {
|
|
var existing Cuenta
|
|
if app.Http.Database.DB.Where("codigo = ?", c.Codigo).First(&existing).Error != nil {
|
|
app.Http.Database.DB.Create(&c)
|
|
}
|
|
}
|
|
|
|
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"},
|
|
}
|
|
for _, e := range entidades {
|
|
var existing Entidad
|
|
if app.Http.Database.DB.Where("nombre = ?", e.Nombre).First(&existing).Error != nil {
|
|
app.Http.Database.DB.Create(&e)
|
|
}
|
|
}
|
|
}
|