From 99d18bb340a23d27bdecef0aea0f234e1c962dfa Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Sun, 25 Jan 2026 10:38:08 -0600 Subject: [PATCH] feat(entities): Complete viajes entities and adapt inventory/financial - Add Viaje entity with transport-specific workflow states - Add ParadaViaje entity for multi-stop support - Add Pod entity for Proof of Delivery - Adapt inventory/product.entity with TipoRefaccion for fleet parts - Adapt financial/account.entity with TipoCuentaTransporte and TipoCentroCosto - Update index exports for all modified modules Co-Authored-By: Claude Opus 4.5 --- .../financial/entities/account.entity.ts | 80 +++++++++ src/modules/financial/entities/index.ts | 2 +- src/modules/inventory/entities/index.ts | 2 +- .../inventory/entities/product.entity.ts | 95 ++++++++++ .../ordenes-transporte/entities/index.ts | 9 + src/modules/viajes/entities/index.ts | 9 + .../viajes/entities/parada-viaje.entity.ts | 124 +++++++++++++ src/modules/viajes/entities/pod.entity.ts | 120 +++++++++++++ src/modules/viajes/entities/viaje.entity.ts | 167 ++++++++++++++++++ 9 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 src/modules/viajes/entities/parada-viaje.entity.ts create mode 100644 src/modules/viajes/entities/pod.entity.ts create mode 100644 src/modules/viajes/entities/viaje.entity.ts diff --git a/src/modules/financial/entities/account.entity.ts b/src/modules/financial/entities/account.entity.ts index 5db7d67..09bc939 100644 --- a/src/modules/financial/entities/account.entity.ts +++ b/src/modules/financial/entities/account.entity.ts @@ -12,6 +12,35 @@ import { import { AccountType } from './account-type.entity.js'; import { Company } from '../../auth/entities/company.entity.js'; +/** + * Tipo de Cuenta de Transporte + * Para clasificar cuentas específicas del giro + */ +export enum TipoCuentaTransporte { + GENERAL = 'GENERAL', // Cuenta genérica + COMBUSTIBLE = 'COMBUSTIBLE', // Gastos de combustible + PEAJES = 'PEAJES', // Cruces de casetas + MANTENIMIENTO = 'MANTENIMIENTO', // Mantenimiento de flota + VIATICOS = 'VIATICOS', // Viáticos y hospedaje + SEGURO = 'SEGURO', // Seguros de unidades/carga + DEPRECIACION = 'DEPRECIACION', // Depreciación de flota + NOMINA_OPERADORES = 'NOMINA_OPERADORES', // Sueldos operadores + INGRESOS_FLETE = 'INGRESOS_FLETE', // Ingresos por fletes + CARRIER = 'CARRIER', // Pagos a carriers terceros +} + +/** + * Tipo de Centro de Costo + */ +export enum TipoCentroCosto { + GENERAL = 'GENERAL', + UNIDAD = 'UNIDAD', // Por unidad/vehículo + RUTA = 'RUTA', // Por ruta/lane + CLIENTE = 'CLIENTE', // Por cliente + OPERADOR = 'OPERADOR', // Por operador + SUCURSAL = 'SUCURSAL', // Por sucursal/patio +} + @Entity({ schema: 'financial', name: 'accounts' }) @Index('idx_accounts_tenant_id', ['tenantId']) @Index('idx_accounts_company_id', ['companyId']) @@ -52,6 +81,57 @@ export class Account { @Column({ type: 'text', nullable: true }) notes: string | null; + // === CAMPOS ESPECÍFICOS PARA TRANSPORTE === + + /** + * Tipo de cuenta específico para operaciones de transporte + */ + @Column({ + type: 'enum', + enum: TipoCuentaTransporte, + nullable: true, + name: 'tipo_cuenta_transporte', + }) + tipoCuentaTransporte: TipoCuentaTransporte | null; + + /** + * Tipo de centro de costo para análisis financiero + */ + @Column({ + type: 'enum', + enum: TipoCentroCosto, + nullable: true, + name: 'tipo_centro_costo', + }) + tipoCentroCosto: TipoCentroCosto | null; + + /** + * ID del centro de costo asociado (unidad, operador, ruta, etc.) + * Referencia dinámica según tipoCentroCosto + */ + @Column({ type: 'uuid', nullable: true, name: 'centro_costo_id' }) + centroCostoId: string | null; + + /** + * Nombre/descripción del centro de costo para reportes + */ + @Column({ type: 'varchar', length: 200, nullable: true, name: 'centro_costo_nombre' }) + centroCostoNombre: string | null; + + /** + * Indica si esta cuenta aplica para liquidaciones de viajes + */ + @Column({ type: 'boolean', default: false, name: 'aplica_liquidacion' }) + aplicaLiquidacion: boolean; + + /** + * Indica si requiere comprobante fiscal (CFDI) + */ + @Column({ type: 'boolean', default: false, name: 'requiere_cfdi' }) + requiereCfdi: boolean; + + // === FIN CAMPOS TRANSPORTE === + // Relations @ManyToOne(() => Company) @JoinColumn({ name: 'company_id' }) diff --git a/src/modules/financial/entities/index.ts b/src/modules/financial/entities/index.ts index df67f1c..97af730 100644 --- a/src/modules/financial/entities/index.ts +++ b/src/modules/financial/entities/index.ts @@ -1,6 +1,6 @@ // Account entities export { AccountType, AccountTypeEnum } from './account-type.entity.js'; -export { Account } from './account.entity.js'; +export { Account, TipoCuentaTransporte, TipoCentroCosto } from './account.entity.js'; export { AccountMapping, AccountMappingType } from './account-mapping.entity.js'; // Journal entities diff --git a/src/modules/inventory/entities/index.ts b/src/modules/inventory/entities/index.ts index 6347284..9d53a9a 100644 --- a/src/modules/inventory/entities/index.ts +++ b/src/modules/inventory/entities/index.ts @@ -1,5 +1,5 @@ // Core Inventory Entities -export { Product } from './product.entity.js'; +export { Product, TipoRefaccion, ProductType, TrackingType, ValuationMethod } from './product.entity.js'; // Re-export Warehouse from canonical location in warehouses module export { Warehouse } from '../../warehouses/entities/warehouse.entity.js'; export { Location } from './location.entity.js'; diff --git a/src/modules/inventory/entities/product.entity.ts b/src/modules/inventory/entities/product.entity.ts index 85a159a..b26ccc2 100644 --- a/src/modules/inventory/entities/product.entity.ts +++ b/src/modules/inventory/entities/product.entity.ts @@ -13,12 +13,18 @@ import { Lot } from './lot.entity.js'; /** * Inventory Product Entity (schema: inventory.products) * + * ADAPTADO PARA ERP TRANSPORTISTAS: + * Este módulo se usa principalmente para gestionar refacciones de flota. + * Incluye campos específicos para identificar partes de vehículos y su + * compatibilidad con unidades de la flota. + * * NOTE: This is NOT a duplicate of products/entities/product.entity.ts * * Key differences: * - This entity: inventory.products - Warehouse/stock management focused (Odoo-style) * - Has: valuationMethod, tracking (lot/serial), isStorable, StockQuant/Lot relations * - Used by: Inventory module for stock tracking, valuation, picking operations + * - TRANSPORT: tipoRefaccion, numeroParte, unidadesCompatibles * * - Products entity: products.products - Commerce/retail focused * - Has: SAT codes, tax rates, detailed dimensions, min/max stock, reorder points @@ -33,6 +39,24 @@ export enum ProductType { SERVICE = 'service', } +/** + * Tipo de Refacción (específico para transporte) + */ +export enum TipoRefaccion { + MOTOR = 'MOTOR', + FRENOS = 'FRENOS', + LLANTAS = 'LLANTAS', + SUSPENSION = 'SUSPENSION', + TRANSMISION = 'TRANSMISION', + ELECTRICO = 'ELECTRICO', + CARROCERIA = 'CARROCERIA', + REFRIGERACION = 'REFRIGERACION', + LUBRICANTES = 'LUBRICANTES', + FILTROS = 'FILTROS', + ACCESORIOS = 'ACCESORIOS', + OTRO = 'OTRO', +} + export enum TrackingType { NONE = 'none', LOT = 'lot', @@ -139,6 +163,77 @@ export class Product { @Column({ type: 'boolean', default: true, nullable: false }) active: boolean; + // === CAMPOS ESPECÍFICOS PARA REFACCIONES DE FLOTA === + + /** + * Tipo de refacción (para clasificación en mantenimiento) + */ + @Column({ + type: 'enum', + enum: TipoRefaccion, + nullable: true, + name: 'tipo_refaccion', + }) + tipoRefaccion: TipoRefaccion | null; + + /** + * Número de parte del fabricante (OEM) + */ + @Column({ type: 'varchar', length: 100, nullable: true, name: 'numero_parte' }) + numeroParte: string | null; + + /** + * Número de parte alterno/equivalente + */ + @Column({ type: 'varchar', length: 100, nullable: true, name: 'numero_parte_alterno' }) + numeroParteAlterno: string | null; + + /** + * Marca del fabricante de la refacción + */ + @Column({ type: 'varchar', length: 100, nullable: true, name: 'marca_refaccion' }) + marcaRefaccion: string | null; + + /** + * IDs de unidades donde esta refacción es compatible + * Referencia a fleet.unidades + */ + @Column({ type: 'uuid', array: true, nullable: true, name: 'unidades_compatibles' }) + unidadesCompatibles: string[] | null; + + /** + * Modelos de vehículos compatibles (texto libre) + * Ej: "Kenworth T680 2020-2024, Freightliner Cascadia" + */ + @Column({ type: 'text', nullable: true, name: 'modelos_compatibles' }) + modelosCompatibles: string | null; + + /** + * Vida útil estimada en kilómetros + */ + @Column({ type: 'int', nullable: true, name: 'vida_util_km' }) + vidaUtilKm: number | null; + + /** + * Vida útil estimada en horas de operación + */ + @Column({ type: 'int', nullable: true, name: 'vida_util_horas' }) + vidaUtilHoras: number | null; + + /** + * Indica si es una refacción crítica (afecta operación) + */ + @Column({ type: 'boolean', default: false, name: 'es_critica' }) + esCritica: boolean; + + /** + * Stock mínimo recomendado para refacciones críticas + */ + @Column({ type: 'int', nullable: true, name: 'stock_minimo_critico' }) + stockMinimoCritico: number | null; + + // === FIN CAMPOS REFACCIONES === + // Relations @OneToMany(() => StockQuant, (stockQuant) => stockQuant.product) stockQuants: StockQuant[]; diff --git a/src/modules/ordenes-transporte/entities/index.ts b/src/modules/ordenes-transporte/entities/index.ts index cca5d8f..5105999 100644 --- a/src/modules/ordenes-transporte/entities/index.ts +++ b/src/modules/ordenes-transporte/entities/index.ts @@ -1,3 +1,12 @@ +/** + * Ordenes de Transporte Entities + * Schema: transport + */ + +// Entities de Transporte +export * from './orden-transporte.entity'; + +// Entities heredadas de sales (para cotizaciones) export { Quotation } from './quotation.entity'; export { QuotationItem } from './quotation-item.entity'; export { SalesOrder } from './sales-order.entity'; diff --git a/src/modules/viajes/entities/index.ts b/src/modules/viajes/entities/index.ts index 94033da..252253a 100644 --- a/src/modules/viajes/entities/index.ts +++ b/src/modules/viajes/entities/index.ts @@ -1 +1,10 @@ +/** + * Viajes Entities + * Schema: transport + */ +export * from './viaje.entity'; +export * from './parada-viaje.entity'; +export * from './pod.entity'; + +// Entities heredadas de projects export * from './timesheet.entity.js'; diff --git a/src/modules/viajes/entities/parada-viaje.entity.ts b/src/modules/viajes/entities/parada-viaje.entity.ts new file mode 100644 index 0000000..5699844 --- /dev/null +++ b/src/modules/viajes/entities/parada-viaje.entity.ts @@ -0,0 +1,124 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Viaje } from './viaje.entity'; + +/** + * Tipo de Parada + */ +export enum TipoParada { + RECOLECCION = 'RECOLECCION', + ENTREGA = 'ENTREGA', + ESCALA = 'ESCALA', +} + +/** + * Estado de la Parada + */ +export enum EstadoParada { + PENDIENTE = 'PENDIENTE', + EN_CAMINO = 'EN_CAMINO', + LLEGADA = 'LLEGADA', + EN_PROCESO = 'EN_PROCESO', + COMPLETADA = 'COMPLETADA', + OMITIDA = 'OMITIDA', +} + +@Entity({ schema: 'transport', name: 'paradas_viaje' }) +@Index('idx_parada_viaje', ['viajeId']) +export class ParadaViaje { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'viaje_id', type: 'uuid' }) + viajeId: string; + + @ManyToOne(() => Viaje) + @JoinColumn({ name: 'viaje_id' }) + viaje: Viaje; + + // Secuencia + @Column({ type: 'int' }) + secuencia: number; + + // Tipo de parada + @Column({ type: 'enum', enum: TipoParada }) + tipo: TipoParada; + + // Ubicación + @Column({ type: 'text' }) + direccion: string; + + @Column({ name: 'codigo_postal', type: 'varchar', length: 10, nullable: true }) + codigoPostal: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + ciudad: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + estado: string; + + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + latitud: number; + + @Column({ type: 'decimal', precision: 10, scale: 7, nullable: true }) + longitud: number; + + // Contacto + @Column({ name: 'contacto_nombre', type: 'varchar', length: 200, nullable: true }) + contactoNombre: string; + + @Column({ name: 'contacto_telefono', type: 'varchar', length: 30, nullable: true }) + contactoTelefono: string; + + // Programación + @Column({ name: 'hora_programada_llegada', type: 'timestamptz', nullable: true }) + horaProgramadaLlegada: Date; + + @Column({ name: 'hora_programada_salida', type: 'timestamptz', nullable: true }) + horaProgramadaSalida: Date; + + // Real + @Column({ name: 'hora_real_llegada', type: 'timestamptz', nullable: true }) + horaRealLlegada: Date; + + @Column({ name: 'hora_real_salida', type: 'timestamptz', nullable: true }) + horaRealSalida: Date; + + // OTs asociadas + @Column({ name: 'ots_ids', type: 'uuid', array: true, nullable: true }) + otsIds: string[]; + + // Estado + @Column({ name: 'estado_parada', type: 'enum', enum: EstadoParada, default: EstadoParada.PENDIENTE }) + estadoParada: EstadoParada; + + // Observaciones + @Column({ type: 'text', nullable: true }) + observaciones: string; + + // Helper para calcular tiempo de permanencia + get tiempoPermanenciaMinutos(): number | null { + if (this.horaRealLlegada && this.horaRealSalida) { + const diff = this.horaRealSalida.getTime() - this.horaRealLlegada.getTime(); + return Math.round(diff / (1000 * 60)); + } + return null; + } + + // Helper para verificar si está retrasada + get estaRetrasada(): boolean { + if (this.horaProgramadaLlegada && this.horaRealLlegada) { + return this.horaRealLlegada > this.horaProgramadaLlegada; + } + return false; + } +} diff --git a/src/modules/viajes/entities/pod.entity.ts b/src/modules/viajes/entities/pod.entity.ts new file mode 100644 index 0000000..91b4003 --- /dev/null +++ b/src/modules/viajes/entities/pod.entity.ts @@ -0,0 +1,120 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + Index, + ManyToOne, + JoinColumn, +} from 'typeorm'; +import { Viaje } from './viaje.entity'; +import { ParadaViaje } from './parada-viaje.entity'; + +/** + * Estado del POD (Proof of Delivery) + */ +export enum EstadoPod { + PENDIENTE = 'PENDIENTE', + PARCIAL = 'PARCIAL', + COMPLETO = 'COMPLETO', + RECHAZADO = 'RECHAZADO', +} + +/** + * Foto de Evidencia + */ +export interface FotoEvidencia { + url: string; + descripcion?: string; + tipo: 'MERCANCIA' | 'FIRMA' | 'DANIO' | 'SELLO' | 'DOCUMENTO' | 'OTRO'; + timestamp: Date; +} + +@Entity({ schema: 'transport', name: 'pod' }) +@Index('idx_pod_viaje', ['viajeId']) +@Index('idx_pod_estado', ['tenantId', 'estado']) +export class Pod { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + @Column({ name: 'viaje_id', type: 'uuid' }) + viajeId: string; + + @ManyToOne(() => Viaje) + @JoinColumn({ name: 'viaje_id' }) + viaje: Viaje; + + @Column({ name: 'parada_id', type: 'uuid', nullable: true }) + paradaId: string; + + @ManyToOne(() => ParadaViaje) + @JoinColumn({ name: 'parada_id' }) + parada: ParadaViaje; + + @Column({ name: 'ot_id', type: 'uuid', nullable: true }) + otId: string; + + // Estado POD + @Column({ type: 'enum', enum: EstadoPod, default: EstadoPod.PENDIENTE }) + estado: EstadoPod; + + // Recepción + @Column({ name: 'receptor_nombre', type: 'varchar', length: 200, nullable: true }) + receptorNombre: string; + + @Column({ name: 'receptor_identificacion', type: 'varchar', length: 50, nullable: true }) + receptorIdentificacion: string; + + @Column({ name: 'fecha_recepcion', type: 'timestamptz', nullable: true }) + fechaRecepcion: Date; + + // Firma digital (Base64 de la imagen de firma) + @Column({ name: 'firma_digital', type: 'text', nullable: true }) + firmaDigital: string; + + // Evidencias (URLs o IDs de archivos) + @Column({ name: 'fotos_entrega', type: 'jsonb', nullable: true }) + fotosEntrega: FotoEvidencia[]; + + // Cantidades + @Column({ name: 'piezas_entregadas', type: 'int', nullable: true }) + piezasEntregadas: number; + + @Column({ name: 'piezas_rechazadas', type: 'int', nullable: true }) + piezasRechazadas: number; + + @Column({ name: 'piezas_danadas', type: 'int', nullable: true }) + piezasDanadas: number; + + // Observaciones + @Column({ type: 'text', nullable: true }) + observaciones: string; + + @Column({ name: 'motivo_rechazo', type: 'text', nullable: true }) + motivoRechazo: string; + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid', nullable: true }) + createdById: string; + + // Helpers + get entregaCompleta(): boolean { + return this.estado === EstadoPod.COMPLETO && + (this.piezasRechazadas || 0) === 0 && + (this.piezasDanadas || 0) === 0; + } + + get tieneFirma(): boolean { + return !!this.firmaDigital; + } + + get tieneEvidenciasFotograficas(): boolean { + return Array.isArray(this.fotosEntrega) && this.fotosEntrega.length > 0; + } +} diff --git a/src/modules/viajes/entities/viaje.entity.ts b/src/modules/viajes/entities/viaje.entity.ts new file mode 100644 index 0000000..c4806d3 --- /dev/null +++ b/src/modules/viajes/entities/viaje.entity.ts @@ -0,0 +1,167 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + Index, + OneToMany, + ManyToOne, + JoinColumn, +} from 'typeorm'; + +/** + * Estado del Viaje + */ +export enum EstadoViaje { + BORRADOR = 'BORRADOR', + PLANEADO = 'PLANEADO', + DESPACHADO = 'DESPACHADO', + EN_TRANSITO = 'EN_TRANSITO', + EN_DESTINO = 'EN_DESTINO', + ENTREGADO = 'ENTREGADO', + CERRADO = 'CERRADO', + FACTURADO = 'FACTURADO', + COBRADO = 'COBRADO', + CANCELADO = 'CANCELADO', +} + +/** + * Información de Sello + */ +export interface InfoSello { + numero: string; + tipo: 'PLASTICO' | 'METALICO' | 'ELECTRONICO' | 'CANDADO'; + colocadoPor?: string; + foto?: string; + timestamp?: Date; +} + +@Entity({ schema: 'transport', name: 'viajes' }) +@Index('idx_viaje_tenant', ['tenantId']) +@Index('idx_viaje_estado', ['tenantId', 'estado']) +@Index('idx_viaje_unidad', ['tenantId', 'unidadId']) +@Index('idx_viaje_operador', ['tenantId', 'operadorId']) +@Index('idx_viaje_fechas', ['tenantId', 'fechaSalidaProgramada']) +export class Viaje { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ name: 'tenant_id', type: 'uuid' }) + tenantId: string; + + // Identificación + @Column({ type: 'varchar', length: 50 }) + codigo: string; + + // Unidad y operador (referencias a fleet schema) + @Column({ name: 'unidad_id', type: 'uuid' }) + unidadId: string; + + @Column({ name: 'remolque_id', type: 'uuid', nullable: true }) + remolqueId: string; + + @Column({ name: 'operador_id', type: 'uuid' }) + operadorId: string; + + // Ruta + @Column({ name: 'origen_principal', type: 'varchar', length: 200, nullable: true }) + origenPrincipal: string; + + @Column({ name: 'destino_principal', type: 'varchar', length: 200, nullable: true }) + destinoPrincipal: string; + + @Column({ name: 'distancia_estimada_km', type: 'decimal', precision: 10, scale: 2, nullable: true }) + distanciaEstimadaKm: number; + + @Column({ name: 'tiempo_estimado_horas', type: 'decimal', precision: 6, scale: 2, nullable: true }) + tiempoEstimadoHoras: number; + + // Fechas programadas + @Column({ name: 'fecha_salida_programada', type: 'timestamptz', nullable: true }) + fechaSalidaProgramada: Date; + + @Column({ name: 'fecha_llegada_programada', type: 'timestamptz', nullable: true }) + fechaLlegadaProgramada: Date; + + // Fechas reales + @Column({ name: 'fecha_salida_real', type: 'timestamptz', nullable: true }) + fechaSalidaReal: Date; + + @Column({ name: 'fecha_llegada_real', type: 'timestamptz', nullable: true }) + fechaLlegadaReal: Date; + + // Kilometraje + @Column({ name: 'km_inicio', type: 'int', nullable: true }) + kmInicio: number; + + @Column({ name: 'km_fin', type: 'int', nullable: true }) + kmFin: number; + + // Calculado: km_recorridos = km_fin - km_inicio + get kmRecorridos(): number | null { + if (this.kmInicio != null && this.kmFin != null) { + return this.kmFin - this.kmInicio; + } + return null; + } + + // Estado + @Column({ type: 'enum', enum: EstadoViaje, default: EstadoViaje.BORRADOR }) + estado: EstadoViaje; + + // Checklist pre-viaje + @Column({ name: 'checklist_completado', type: 'boolean', default: false }) + checklistCompletado: boolean; + + @Column({ name: 'checklist_fecha', type: 'timestamptz', nullable: true }) + checklistFecha: Date; + + @Column({ name: 'checklist_observaciones', type: 'text', nullable: true }) + checklistObservaciones: string; + + // Sellos + @Column({ name: 'sellos_salida', type: 'jsonb', nullable: true }) + sellosSalida: InfoSello[]; + + @Column({ name: 'sellos_llegada', type: 'jsonb', nullable: true }) + sellosLlegada: InfoSello[]; + + // Costos + @Column({ name: 'costo_combustible', type: 'decimal', precision: 15, scale: 2, default: 0 }) + costoCombustible: number; + + @Column({ name: 'costo_peajes', type: 'decimal', precision: 15, scale: 2, default: 0 }) + costoPeajes: number; + + @Column({ name: 'costo_viaticos', type: 'decimal', precision: 15, scale: 2, default: 0 }) + costoViaticos: number; + + @Column({ name: 'costo_otros', type: 'decimal', precision: 15, scale: 2, default: 0 }) + costoOtros: number; + + @Column({ name: 'costo_total', type: 'decimal', precision: 15, scale: 2, default: 0 }) + costoTotal: number; + + // Ingresos + @Column({ name: 'ingreso_total', type: 'decimal', precision: 15, scale: 2, default: 0 }) + ingresoTotal: number; + + // Calculado: margen = ingreso_total - costo_total + get margen(): number { + return (this.ingresoTotal || 0) - (this.costoTotal || 0); + } + + // Auditoría + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + createdAt: Date; + + @Column({ name: 'created_by_id', type: 'uuid' }) + createdById: string; + + @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' }) + updatedAt: Date; + + @Column({ name: 'updated_by_id', type: 'uuid', nullable: true }) + updatedById: string; +}