feat: Add combustible-gastos and tarifas-transporte entities
New entities for combustible-gastos module: - anticipo-viatico.entity.ts - carga-combustible.entity.ts - control-rendimiento.entity.ts - cruce-peaje.entity.ts - gasto-viaje.entity.ts New entities for tarifas-transporte module: - factura-transporte.entity.ts - fuel-surcharge.entity.ts - lane.entity.ts - linea-factura.entity.ts - recargo-catalogo.entity.ts - tarifa.entity.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
99d18bb340
commit
2722920e12
@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estado del Anticipo
|
||||||
|
*/
|
||||||
|
export enum EstadoAnticipo {
|
||||||
|
SOLICITADO = 'SOLICITADO',
|
||||||
|
APROBADO = 'APROBADO',
|
||||||
|
ENTREGADO = 'ENTREGADO',
|
||||||
|
COMPROBANDO = 'COMPROBANDO',
|
||||||
|
LIQUIDADO = 'LIQUIDADO',
|
||||||
|
RECHAZADO = 'RECHAZADO',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'fuel', name: 'anticipos_viaticos' })
|
||||||
|
@Index('idx_anticipo_viaje', ['viajeId'])
|
||||||
|
@Index('idx_anticipo_operador', ['operadorId'])
|
||||||
|
@Index('idx_anticipo_estado', ['tenantId', 'estado'])
|
||||||
|
export class AnticipoViatico {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Viaje y operador
|
||||||
|
@Column({ name: 'viaje_id', type: 'uuid' })
|
||||||
|
viajeId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'operador_id', type: 'uuid' })
|
||||||
|
operadorId: string;
|
||||||
|
|
||||||
|
// Anticipo
|
||||||
|
@Column({ name: 'monto_solicitado', type: 'decimal', precision: 12, scale: 2 })
|
||||||
|
montoSolicitado: number;
|
||||||
|
|
||||||
|
@Column({ name: 'monto_aprobado', type: 'decimal', precision: 12, scale: 2, nullable: true })
|
||||||
|
montoAprobado: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'monto_comprobado', type: 'decimal', precision: 12, scale: 2, default: 0 })
|
||||||
|
montoComprobado: number;
|
||||||
|
|
||||||
|
@Column({ name: 'monto_reintegro', type: 'decimal', precision: 12, scale: 2, default: 0 })
|
||||||
|
montoReintegro: number;
|
||||||
|
|
||||||
|
// Conceptos desglosados
|
||||||
|
@Column({ name: 'combustible_estimado', type: 'decimal', precision: 12, scale: 2, nullable: true })
|
||||||
|
combustibleEstimado: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'peajes_estimado', type: 'decimal', precision: 12, scale: 2, nullable: true })
|
||||||
|
peajesEstimado: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'viaticos_estimado', type: 'decimal', precision: 12, scale: 2, nullable: true })
|
||||||
|
viaticosEstimado: number | null;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'varchar', length: 20, default: EstadoAnticipo.SOLICITADO })
|
||||||
|
estado: string;
|
||||||
|
|
||||||
|
// Fechas
|
||||||
|
@Column({ name: 'fecha_solicitud', type: 'timestamptz', default: () => 'NOW()' })
|
||||||
|
fechaSolicitud: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_aprobacion', type: 'timestamptz', nullable: true })
|
||||||
|
fechaAprobacion: Date | null;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_entrega', type: 'timestamptz', nullable: true })
|
||||||
|
fechaEntrega: Date | null;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_liquidacion', type: 'timestamptz', nullable: true })
|
||||||
|
fechaLiquidacion: Date | null;
|
||||||
|
|
||||||
|
// Aprobaciones
|
||||||
|
@Column({ name: 'aprobado_por', type: 'uuid', nullable: true })
|
||||||
|
aprobadoPor: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'entregado_por', type: 'uuid', nullable: true })
|
||||||
|
entregadoPor: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'liquidado_por', type: 'uuid', nullable: true })
|
||||||
|
liquidadoPor: string | null;
|
||||||
|
|
||||||
|
// Observaciones
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
observaciones: string | null;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'created_by_id', type: 'uuid' })
|
||||||
|
createdById: string;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get saldoPendiente(): number {
|
||||||
|
const aprobado = this.montoAprobado || 0;
|
||||||
|
return aprobado - this.montoComprobado;
|
||||||
|
}
|
||||||
|
|
||||||
|
get porcentajeComprobado(): number {
|
||||||
|
const aprobado = this.montoAprobado || 0;
|
||||||
|
return aprobado > 0 ? (this.montoComprobado / aprobado) * 100 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get requiereReintegro(): boolean {
|
||||||
|
return this.montoReintegro > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de Carga de Combustible
|
||||||
|
*/
|
||||||
|
export enum TipoCargaCombustible {
|
||||||
|
VALE = 'VALE',
|
||||||
|
TARJETA = 'TARJETA',
|
||||||
|
EFECTIVO = 'EFECTIVO',
|
||||||
|
FACTURA_DIRECTA = 'FACTURA_DIRECTA',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de Combustible
|
||||||
|
*/
|
||||||
|
export enum TipoCombustible {
|
||||||
|
DIESEL = 'DIESEL',
|
||||||
|
GASOLINA_MAGNA = 'GASOLINA_MAGNA',
|
||||||
|
GASOLINA_PREMIUM = 'GASOLINA_PREMIUM',
|
||||||
|
GAS_LP = 'GAS_LP',
|
||||||
|
GAS_NATURAL = 'GAS_NATURAL',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estado del Gasto
|
||||||
|
*/
|
||||||
|
export enum EstadoGasto {
|
||||||
|
PENDIENTE = 'PENDIENTE',
|
||||||
|
APROBADO = 'APROBADO',
|
||||||
|
RECHAZADO = 'RECHAZADO',
|
||||||
|
PAGADO = 'PAGADO',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'fuel', name: 'cargas_combustible' })
|
||||||
|
@Index('idx_carga_unidad', ['unidadId'])
|
||||||
|
@Index('idx_carga_viaje', ['viajeId'])
|
||||||
|
@Index('idx_carga_fecha', ['tenantId', 'fechaCarga'])
|
||||||
|
export class CargaCombustible {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Unidad y viaje
|
||||||
|
@Column({ name: 'unidad_id', type: 'uuid' })
|
||||||
|
unidadId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'viaje_id', type: 'uuid', nullable: true })
|
||||||
|
viajeId: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'operador_id', type: 'uuid' })
|
||||||
|
operadorId: string;
|
||||||
|
|
||||||
|
// Carga
|
||||||
|
@Column({ name: 'tipo_carga', type: 'enum', enum: TipoCargaCombustible })
|
||||||
|
tipoCarga: TipoCargaCombustible;
|
||||||
|
|
||||||
|
@Column({ name: 'tipo_combustible', type: 'varchar', length: 20 })
|
||||||
|
tipoCombustible: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 3 })
|
||||||
|
litros: number;
|
||||||
|
|
||||||
|
@Column({ name: 'precio_litro', type: 'decimal', precision: 10, scale: 4 })
|
||||||
|
precioLitro: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
// Odómetro
|
||||||
|
@Column({ name: 'odometro_carga', type: 'int', nullable: true })
|
||||||
|
odometroCarga: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'rendimiento_calculado', type: 'decimal', precision: 6, scale: 2, nullable: true })
|
||||||
|
rendimientoCalculado: number | null;
|
||||||
|
|
||||||
|
// Ubicación
|
||||||
|
@Column({ name: 'estacion_id', type: 'uuid', nullable: true })
|
||||||
|
estacionId: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'estacion_nombre', type: 'varchar', length: 200, nullable: true })
|
||||||
|
estacionNombre: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'estacion_direccion', type: 'text', nullable: true })
|
||||||
|
estacionDireccion: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||||
|
latitud: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||||
|
longitud: number | null;
|
||||||
|
|
||||||
|
// Vale/Factura
|
||||||
|
@Column({ name: 'numero_vale', type: 'varchar', length: 50, nullable: true })
|
||||||
|
numeroVale: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'numero_factura', type: 'varchar', length: 50, nullable: true })
|
||||||
|
numeroFactura: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'folio_ticket', type: 'varchar', length: 50, nullable: true })
|
||||||
|
folioTicket: string | null;
|
||||||
|
|
||||||
|
// Fecha
|
||||||
|
@Column({ name: 'fecha_carga', type: 'timestamptz' })
|
||||||
|
fechaCarga: Date;
|
||||||
|
|
||||||
|
// Aprobación
|
||||||
|
@Column({ type: 'enum', enum: EstadoGasto, default: EstadoGasto.PENDIENTE })
|
||||||
|
estado: EstadoGasto;
|
||||||
|
|
||||||
|
@Column({ name: 'aprobado_por', type: 'uuid', nullable: true })
|
||||||
|
aprobadoPor: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'aprobado_fecha', type: 'timestamptz', nullable: true })
|
||||||
|
aprobadoFecha: Date | null;
|
||||||
|
|
||||||
|
// Evidencia
|
||||||
|
@Column({ name: 'foto_ticket_url', type: 'text', nullable: true })
|
||||||
|
fotoTicketUrl: string | null;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'created_by_id', type: 'uuid' })
|
||||||
|
createdById: string;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get costoPorLitro(): number {
|
||||||
|
return this.litros > 0 ? this.total / this.litros : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de Anomalía en Rendimiento
|
||||||
|
*/
|
||||||
|
export enum TipoAnomaliaRendimiento {
|
||||||
|
BAJO_RENDIMIENTO = 'BAJO_RENDIMIENTO',
|
||||||
|
CONSUMO_EXCESIVO = 'CONSUMO_EXCESIVO',
|
||||||
|
POSIBLE_ROBO = 'POSIBLE_ROBO',
|
||||||
|
FALLA_MECANICA = 'FALLA_MECANICA',
|
||||||
|
RUTA_INEFICIENTE = 'RUTA_INEFICIENTE',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'fuel', name: 'control_rendimiento' })
|
||||||
|
@Index('idx_rendimiento_unidad', ['unidadId'])
|
||||||
|
@Index('idx_rendimiento_fecha', ['tenantId', 'fechaInicio'])
|
||||||
|
export class ControlRendimiento {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Unidad
|
||||||
|
@Column({ name: 'unidad_id', type: 'uuid' })
|
||||||
|
unidadId: string;
|
||||||
|
|
||||||
|
// Período
|
||||||
|
@Column({ name: 'fecha_inicio', type: 'date' })
|
||||||
|
fechaInicio: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_fin', type: 'date' })
|
||||||
|
fechaFin: Date;
|
||||||
|
|
||||||
|
// Métricas
|
||||||
|
@Column({ name: 'km_recorridos', type: 'int' })
|
||||||
|
kmRecorridos: number;
|
||||||
|
|
||||||
|
@Column({ name: 'litros_consumidos', type: 'decimal', precision: 12, scale: 3 })
|
||||||
|
litrosConsumidos: number;
|
||||||
|
|
||||||
|
@Column({ name: 'rendimiento_real', type: 'decimal', precision: 6, scale: 2 })
|
||||||
|
rendimientoReal: number;
|
||||||
|
|
||||||
|
@Column({ name: 'rendimiento_esperado', type: 'decimal', precision: 6, scale: 2, nullable: true })
|
||||||
|
rendimientoEsperado: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'variacion_porcentaje', type: 'decimal', precision: 5, scale: 2, nullable: true })
|
||||||
|
variacionPorcentaje: number | null;
|
||||||
|
|
||||||
|
// Costos
|
||||||
|
@Column({ name: 'costo_total_combustible', type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||||
|
costoTotalCombustible: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'costo_por_km', type: 'decimal', precision: 8, scale: 4, nullable: true })
|
||||||
|
costoPorKm: number | null;
|
||||||
|
|
||||||
|
// Alertas
|
||||||
|
@Column({ name: 'tiene_anomalia', type: 'boolean', default: false })
|
||||||
|
tieneAnomalia: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'tipo_anomalia', type: 'varchar', length: 50, nullable: true })
|
||||||
|
tipoAnomalia: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'descripcion_anomalia', type: 'text', nullable: true })
|
||||||
|
descripcionAnomalia: string | null;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get eficiencia(): string {
|
||||||
|
if (!this.rendimientoEsperado) return 'SIN_REFERENCIA';
|
||||||
|
const ratio = this.rendimientoReal / this.rendimientoEsperado;
|
||||||
|
if (ratio >= 0.95) return 'OPTIMO';
|
||||||
|
if (ratio >= 0.85) return 'ACEPTABLE';
|
||||||
|
if (ratio >= 0.75) return 'BAJO';
|
||||||
|
return 'CRITICO';
|
||||||
|
}
|
||||||
|
|
||||||
|
get diasPeriodo(): number {
|
||||||
|
const inicio = new Date(this.fechaInicio);
|
||||||
|
const fin = new Date(this.fechaFin);
|
||||||
|
return Math.ceil((fin.getTime() - inicio.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { EstadoGasto } from './carga-combustible.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de Pago de Peaje
|
||||||
|
*/
|
||||||
|
export enum TipoPagoPeaje {
|
||||||
|
EFECTIVO = 'EFECTIVO',
|
||||||
|
TAG = 'TAG',
|
||||||
|
PREPAGO = 'PREPAGO',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'fuel', name: 'cruces_peaje' })
|
||||||
|
@Index('idx_peaje_unidad', ['unidadId'])
|
||||||
|
@Index('idx_peaje_viaje', ['viajeId'])
|
||||||
|
@Index('idx_peaje_fecha', ['tenantId', 'fechaCruce'])
|
||||||
|
export class CrucePeaje {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Unidad y viaje
|
||||||
|
@Column({ name: 'unidad_id', type: 'uuid' })
|
||||||
|
unidadId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'viaje_id', type: 'uuid', nullable: true })
|
||||||
|
viajeId: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'operador_id', type: 'uuid', nullable: true })
|
||||||
|
operadorId: string | null;
|
||||||
|
|
||||||
|
// Peaje
|
||||||
|
@Column({ name: 'caseta_nombre', type: 'varchar', length: 200 })
|
||||||
|
casetaNombre: string;
|
||||||
|
|
||||||
|
@Column({ name: 'caseta_codigo', type: 'varchar', length: 50, nullable: true })
|
||||||
|
casetaCodigo: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 200, nullable: true })
|
||||||
|
carretera: string | null;
|
||||||
|
|
||||||
|
// Monto
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||||
|
monto: number;
|
||||||
|
|
||||||
|
@Column({ name: 'tipo_pago', type: 'varchar', length: 20, nullable: true })
|
||||||
|
tipoPago: string | null;
|
||||||
|
|
||||||
|
// TAG
|
||||||
|
@Column({ name: 'tag_numero', type: 'varchar', length: 50, nullable: true })
|
||||||
|
tagNumero: string | null;
|
||||||
|
|
||||||
|
// Ubicación
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||||
|
latitud: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||||
|
longitud: number | null;
|
||||||
|
|
||||||
|
// Fecha
|
||||||
|
@Column({ name: 'fecha_cruce', type: 'timestamptz' })
|
||||||
|
fechaCruce: Date;
|
||||||
|
|
||||||
|
// Comprobante
|
||||||
|
@Column({ name: 'numero_ticket', type: 'varchar', length: 50, nullable: true })
|
||||||
|
numeroTicket: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'foto_ticket_url', type: 'text', nullable: true })
|
||||||
|
fotoTicketUrl: string | null;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'enum', enum: EstadoGasto, default: EstadoGasto.APROBADO })
|
||||||
|
estado: EstadoGasto;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
109
src/modules/combustible-gastos/entities/gasto-viaje.entity.ts
Normal file
109
src/modules/combustible-gastos/entities/gasto-viaje.entity.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { EstadoGasto } from './carga-combustible.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de Gasto de Viaje
|
||||||
|
*/
|
||||||
|
export enum TipoGasto {
|
||||||
|
COMBUSTIBLE = 'COMBUSTIBLE',
|
||||||
|
PEAJE = 'PEAJE',
|
||||||
|
VIATICO = 'VIATICO',
|
||||||
|
HOSPEDAJE = 'HOSPEDAJE',
|
||||||
|
ALIMENTOS = 'ALIMENTOS',
|
||||||
|
ESTACIONAMIENTO = 'ESTACIONAMIENTO',
|
||||||
|
MULTA = 'MULTA',
|
||||||
|
MANIOBRA = 'MANIOBRA',
|
||||||
|
REPARACION_MENOR = 'REPARACION_MENOR',
|
||||||
|
OTRO = 'OTRO',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'fuel', name: 'gastos_viaje' })
|
||||||
|
@Index('idx_gasto_viaje', ['viajeId'])
|
||||||
|
@Index('idx_gasto_operador', ['operadorId'])
|
||||||
|
@Index('idx_gasto_estado', ['tenantId', 'estado'])
|
||||||
|
export class GastoViaje {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Viaje y operador
|
||||||
|
@Column({ name: 'viaje_id', type: 'uuid' })
|
||||||
|
viajeId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'operador_id', type: 'uuid' })
|
||||||
|
operadorId: string;
|
||||||
|
|
||||||
|
// Gasto
|
||||||
|
@Column({ name: 'tipo_gasto', type: 'enum', enum: TipoGasto })
|
||||||
|
tipoGasto: TipoGasto;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 500 })
|
||||||
|
descripcion: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||||
|
monto: number;
|
||||||
|
|
||||||
|
// Comprobante
|
||||||
|
@Column({ name: 'tiene_factura', type: 'boolean', default: false })
|
||||||
|
tieneFactura: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'numero_factura', type: 'varchar', length: 50, nullable: true })
|
||||||
|
numeroFactura: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'numero_ticket', type: 'varchar', length: 50, nullable: true })
|
||||||
|
numeroTicket: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'foto_comprobante_url', type: 'text', nullable: true })
|
||||||
|
fotoComprobanteUrl: string | null;
|
||||||
|
|
||||||
|
// Ubicación
|
||||||
|
@Column({ type: 'varchar', length: 200, nullable: true })
|
||||||
|
lugar: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||||
|
latitud: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 10, scale: 7, nullable: true })
|
||||||
|
longitud: number | null;
|
||||||
|
|
||||||
|
// Fecha
|
||||||
|
@Column({ name: 'fecha_gasto', type: 'timestamptz' })
|
||||||
|
fechaGasto: Date;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'enum', enum: EstadoGasto, default: EstadoGasto.PENDIENTE })
|
||||||
|
estado: EstadoGasto;
|
||||||
|
|
||||||
|
@Column({ name: 'aprobado_por', type: 'uuid', nullable: true })
|
||||||
|
aprobadoPor: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'aprobado_fecha', type: 'timestamptz', nullable: true })
|
||||||
|
aprobadoFecha: Date | null;
|
||||||
|
|
||||||
|
@Column({ name: 'motivo_rechazo', type: 'text', nullable: true })
|
||||||
|
motivoRechazo: string | null;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'created_by_id', type: 'uuid' })
|
||||||
|
createdById: string;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get esDeducible(): boolean {
|
||||||
|
return this.tieneFactura;
|
||||||
|
}
|
||||||
|
|
||||||
|
get requiereAprobacion(): boolean {
|
||||||
|
return this.estado === EstadoGasto.PENDIENTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Combustible y Gastos Entities
|
* Combustible y Gastos Entities
|
||||||
|
* Schema: fuel
|
||||||
*/
|
*/
|
||||||
// TODO: Implement entities
|
export * from './carga-combustible.entity';
|
||||||
// - carga-combustible.entity.ts
|
export * from './cruce-peaje.entity';
|
||||||
// - cruce-peaje.entity.ts
|
export * from './gasto-viaje.entity';
|
||||||
// - gasto-viaje.entity.ts
|
export * from './anticipo-viatico.entity';
|
||||||
// - viatico.entity.ts
|
export * from './control-rendimiento.entity';
|
||||||
|
|||||||
@ -0,0 +1,173 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
OneToMany,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { LineaFactura } from './linea-factura.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estado de Factura
|
||||||
|
*/
|
||||||
|
export enum EstadoFactura {
|
||||||
|
BORRADOR = 'BORRADOR',
|
||||||
|
EMITIDA = 'EMITIDA',
|
||||||
|
ENVIADA = 'ENVIADA',
|
||||||
|
PAGADA = 'PAGADA',
|
||||||
|
PARCIAL = 'PARCIAL',
|
||||||
|
VENCIDA = 'VENCIDA',
|
||||||
|
CANCELADA = 'CANCELADA',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'billing', name: 'facturas_transporte' })
|
||||||
|
@Index('idx_factura_tenant', ['tenantId'])
|
||||||
|
@Index('idx_factura_cliente', ['clienteId'])
|
||||||
|
@Index('idx_factura_estado', ['tenantId', 'estado'])
|
||||||
|
@Index('idx_factura_fecha', ['fechaEmision'])
|
||||||
|
@Index('idx_factura_folio', ['tenantId', 'serie', 'folio'], { unique: true })
|
||||||
|
export class FacturaTransporte {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Identificación
|
||||||
|
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||||
|
serie: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 20 })
|
||||||
|
folio: string;
|
||||||
|
|
||||||
|
@Column({ name: 'uuid_cfdi', type: 'uuid', nullable: true })
|
||||||
|
uuidCfdi: string | null;
|
||||||
|
|
||||||
|
// Cliente
|
||||||
|
@Column({ name: 'cliente_id', type: 'uuid' })
|
||||||
|
clienteId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'cliente_rfc', type: 'varchar', length: 13 })
|
||||||
|
clienteRfc: string;
|
||||||
|
|
||||||
|
@Column({ name: 'cliente_razon_social', type: 'varchar', length: 200 })
|
||||||
|
clienteRazonSocial: string;
|
||||||
|
|
||||||
|
@Column({ name: 'cliente_uso_cfdi', type: 'varchar', length: 10, nullable: true })
|
||||||
|
clienteUsoCfdi: string | null;
|
||||||
|
|
||||||
|
// Fechas
|
||||||
|
@Column({ name: 'fecha_emision', type: 'timestamptz' })
|
||||||
|
fechaEmision: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_vencimiento', type: 'date', nullable: true })
|
||||||
|
fechaVencimiento: Date | null;
|
||||||
|
|
||||||
|
// Totales
|
||||||
|
@Column({ type: 'decimal', precision: 15, scale: 2 })
|
||||||
|
subtotal: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||||
|
descuento: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||||
|
iva: number;
|
||||||
|
|
||||||
|
@Column({ name: 'retencion_iva', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||||
|
retencionIva: number;
|
||||||
|
|
||||||
|
@Column({ name: 'retencion_isr', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||||
|
retencionIsr: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 15, scale: 2 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||||
|
moneda: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tipo_cambio', type: 'decimal', precision: 10, scale: 4, default: 1 })
|
||||||
|
tipoCambio: number;
|
||||||
|
|
||||||
|
// Pago
|
||||||
|
@Column({ name: 'forma_pago', type: 'varchar', length: 10, nullable: true })
|
||||||
|
formaPago: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'metodo_pago', type: 'varchar', length: 10, nullable: true })
|
||||||
|
metodoPago: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'condiciones_pago', type: 'varchar', length: 200, nullable: true })
|
||||||
|
condicionesPago: string | null;
|
||||||
|
|
||||||
|
// Relacionados
|
||||||
|
@Column({ name: 'viaje_ids', type: 'uuid', array: true, nullable: true })
|
||||||
|
viajeIds: string[] | null;
|
||||||
|
|
||||||
|
@Column({ name: 'ot_ids', type: 'uuid', array: true, nullable: true })
|
||||||
|
otIds: string[] | null;
|
||||||
|
|
||||||
|
// CFDI
|
||||||
|
@Column({ name: 'xml_cfdi', type: 'text', nullable: true })
|
||||||
|
xmlCfdi: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'pdf_url', type: 'text', nullable: true })
|
||||||
|
pdfUrl: string | null;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'enum', enum: EstadoFactura, default: EstadoFactura.BORRADOR })
|
||||||
|
estado: EstadoFactura;
|
||||||
|
|
||||||
|
// Pago
|
||||||
|
@Column({ name: 'monto_pagado', type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||||
|
montoPagado: number;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_pago', type: 'timestamptz', nullable: true })
|
||||||
|
fechaPago: Date | null;
|
||||||
|
|
||||||
|
// Cancelación
|
||||||
|
@Column({ name: 'fecha_cancelacion', type: 'timestamptz', nullable: true })
|
||||||
|
fechaCancelacion: Date | null;
|
||||||
|
|
||||||
|
@Column({ name: 'motivo_cancelacion', type: 'text', nullable: true })
|
||||||
|
motivoCancelacion: string | null;
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
@OneToMany(() => LineaFactura, (linea) => linea.factura)
|
||||||
|
lineas: LineaFactura[];
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get saldoPendiente(): number {
|
||||||
|
return this.total - this.montoPagado;
|
||||||
|
}
|
||||||
|
|
||||||
|
get estaPagada(): boolean {
|
||||||
|
return this.estado === EstadoFactura.PAGADA || this.montoPagado >= this.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
get estaVencida(): boolean {
|
||||||
|
if (!this.fechaVencimiento || this.estaPagada) return false;
|
||||||
|
return new Date() > new Date(this.fechaVencimiento);
|
||||||
|
}
|
||||||
|
|
||||||
|
get diasVencida(): number {
|
||||||
|
if (!this.fechaVencimiento || !this.estaVencida) return 0;
|
||||||
|
const hoy = new Date();
|
||||||
|
const vencimiento = new Date(this.fechaVencimiento);
|
||||||
|
return Math.ceil((hoy.getTime() - vencimiento.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
get folioCompleto(): string {
|
||||||
|
return this.serie ? `${this.serie}-${this.folio}` : this.folio;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ schema: 'billing', name: 'fuel_surcharge' })
|
||||||
|
@Index('idx_fuel_surcharge_fecha', ['tenantId', 'fechaInicio', 'fechaFin'])
|
||||||
|
export class FuelSurcharge {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Período
|
||||||
|
@Column({ name: 'fecha_inicio', type: 'date' })
|
||||||
|
fechaInicio: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_fin', type: 'date' })
|
||||||
|
fechaFin: Date;
|
||||||
|
|
||||||
|
// Precios de referencia
|
||||||
|
@Column({ name: 'precio_diesel_referencia', type: 'decimal', precision: 10, scale: 4, nullable: true })
|
||||||
|
precioDieselReferencia: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'precio_diesel_actual', type: 'decimal', precision: 10, scale: 4, nullable: true })
|
||||||
|
precioDieselActual: number | null;
|
||||||
|
|
||||||
|
// Surcharge
|
||||||
|
@Column({ name: 'porcentaje_surcharge', type: 'decimal', precision: 5, scale: 2 })
|
||||||
|
porcentajeSurcharge: number;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
activo: boolean;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'created_by_id', type: 'uuid' })
|
||||||
|
createdById: string;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get vigente(): boolean {
|
||||||
|
const hoy = new Date();
|
||||||
|
const inicio = new Date(this.fechaInicio);
|
||||||
|
const fin = new Date(this.fechaFin);
|
||||||
|
return this.activo && hoy >= inicio && hoy <= fin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get variacionPrecio(): number | null {
|
||||||
|
if (!this.precioDieselReferencia || !this.precioDieselActual) return null;
|
||||||
|
return ((this.precioDieselActual - this.precioDieselReferencia) / this.precioDieselReferencia) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcularRecargo(montoBase: number): number {
|
||||||
|
return montoBase * (this.porcentajeSurcharge / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
get descripcionPeriodo(): string {
|
||||||
|
const inicio = new Date(this.fechaInicio).toLocaleDateString('es-MX');
|
||||||
|
const fin = new Date(this.fechaFin).toLocaleDateString('es-MX');
|
||||||
|
return `${inicio} - ${fin}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Tarifas Entities
|
* Tarifas de Transporte Entities
|
||||||
|
* Schema: billing
|
||||||
*/
|
*/
|
||||||
// TODO: Implement entities
|
export * from './lane.entity';
|
||||||
// - tarifa.entity.ts
|
export * from './tarifa.entity';
|
||||||
// - recargo.entity.ts
|
export * from './recargo-catalogo.entity';
|
||||||
// - contrato-tarifa.entity.ts
|
export * from './factura-transporte.entity';
|
||||||
// - lane.entity.ts
|
export * from './linea-factura.entity';
|
||||||
|
export * from './fuel-surcharge.entity';
|
||||||
|
|||||||
72
src/modules/tarifas-transporte/entities/lane.entity.ts
Normal file
72
src/modules/tarifas-transporte/entities/lane.entity.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ schema: 'billing', name: 'lanes' })
|
||||||
|
@Index('idx_lane_tenant', ['tenantId'])
|
||||||
|
@Index('idx_lane_codigo', ['tenantId', 'codigo'], { unique: true })
|
||||||
|
export class Lane {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Identificación
|
||||||
|
@Column({ type: 'varchar', length: 50 })
|
||||||
|
codigo: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 200 })
|
||||||
|
nombre: string;
|
||||||
|
|
||||||
|
// Origen
|
||||||
|
@Column({ name: 'origen_ciudad', type: 'varchar', length: 100 })
|
||||||
|
origenCiudad: string;
|
||||||
|
|
||||||
|
@Column({ name: 'origen_estado', type: 'varchar', length: 100 })
|
||||||
|
origenEstado: string;
|
||||||
|
|
||||||
|
@Column({ name: 'origen_codigo_postal', type: 'varchar', length: 10, nullable: true })
|
||||||
|
origenCodigoPostal: string | null;
|
||||||
|
|
||||||
|
// Destino
|
||||||
|
@Column({ name: 'destino_ciudad', type: 'varchar', length: 100 })
|
||||||
|
destinoCiudad: string;
|
||||||
|
|
||||||
|
@Column({ name: 'destino_estado', type: 'varchar', length: 100 })
|
||||||
|
destinoEstado: string;
|
||||||
|
|
||||||
|
@Column({ name: 'destino_codigo_postal', type: 'varchar', length: 10, nullable: true })
|
||||||
|
destinoCodigoPostal: string | null;
|
||||||
|
|
||||||
|
// Distancia
|
||||||
|
@Column({ name: 'distancia_km', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
distanciaKm: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'tiempo_estimado_horas', type: 'decimal', precision: 6, scale: 2, nullable: true })
|
||||||
|
tiempoEstimadoHoras: number | null;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
activo: boolean;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'created_by_id', type: 'uuid' })
|
||||||
|
createdById: string;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get descripcionCorta(): string {
|
||||||
|
return `${this.origenCiudad} → ${this.destinoCiudad}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get descripcionCompleta(): string {
|
||||||
|
return `${this.origenCiudad}, ${this.origenEstado} → ${this.destinoCiudad}, ${this.destinoEstado}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
Index,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { FacturaTransporte } from './factura-transporte.entity';
|
||||||
|
import { RecargoCatalogo } from './recargo-catalogo.entity';
|
||||||
|
|
||||||
|
@Entity({ schema: 'billing', name: 'lineas_factura' })
|
||||||
|
@Index('idx_linea_factura', ['facturaId'])
|
||||||
|
export class LineaFactura {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'factura_id', type: 'uuid' })
|
||||||
|
facturaId: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => FacturaTransporte, (factura) => factura.lineas)
|
||||||
|
@JoinColumn({ name: 'factura_id' })
|
||||||
|
factura: FacturaTransporte;
|
||||||
|
|
||||||
|
// Secuencia
|
||||||
|
@Column({ type: 'int' })
|
||||||
|
linea: number;
|
||||||
|
|
||||||
|
// Concepto
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
descripcion: string;
|
||||||
|
|
||||||
|
@Column({ name: 'clave_producto_sat', type: 'varchar', length: 10, nullable: true })
|
||||||
|
claveProductoSat: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'unidad_sat', type: 'varchar', length: 10, nullable: true })
|
||||||
|
unidadSat: string | null;
|
||||||
|
|
||||||
|
// Cantidades
|
||||||
|
@Column({ type: 'decimal', precision: 12, scale: 4 })
|
||||||
|
cantidad: number;
|
||||||
|
|
||||||
|
@Column({ name: 'precio_unitario', type: 'decimal', precision: 15, scale: 4 })
|
||||||
|
precioUnitario: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 15, scale: 2, default: 0 })
|
||||||
|
descuento: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 15, scale: 2 })
|
||||||
|
importe: number;
|
||||||
|
|
||||||
|
// Impuestos
|
||||||
|
@Column({ name: 'iva_tasa', type: 'decimal', precision: 5, scale: 2, default: 16 })
|
||||||
|
ivaTasa: number;
|
||||||
|
|
||||||
|
@Column({ name: 'iva_monto', type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||||
|
ivaMonto: number | null;
|
||||||
|
|
||||||
|
// Referencia
|
||||||
|
@Column({ name: 'viaje_id', type: 'uuid', nullable: true })
|
||||||
|
viajeId: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'ot_id', type: 'uuid', nullable: true })
|
||||||
|
otId: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'recargo_id', type: 'uuid', nullable: true })
|
||||||
|
recargoId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => RecargoCatalogo)
|
||||||
|
@JoinColumn({ name: 'recargo_id' })
|
||||||
|
recargo: RecargoCatalogo | null;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get importeNeto(): number {
|
||||||
|
return this.importe - this.descuento;
|
||||||
|
}
|
||||||
|
|
||||||
|
get importeConIva(): number {
|
||||||
|
return this.importeNeto + (this.ivaMonto || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
calcularImporte(): number {
|
||||||
|
return this.cantidad * this.precioUnitario;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcularIva(): number {
|
||||||
|
return this.importeNeto * (this.ivaTasa / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de Recargo
|
||||||
|
*/
|
||||||
|
export enum TipoRecargo {
|
||||||
|
FUEL_SURCHARGE = 'FUEL_SURCHARGE',
|
||||||
|
DETENTION = 'DETENTION',
|
||||||
|
MANIOBRAS = 'MANIOBRAS',
|
||||||
|
CUSTODIA = 'CUSTODIA',
|
||||||
|
ESCOLTA = 'ESCOLTA',
|
||||||
|
PERNOCTA = 'PERNOCTA',
|
||||||
|
ESTADIAS = 'ESTADIAS',
|
||||||
|
FALSO_FLETE = 'FALSO_FLETE',
|
||||||
|
SEGURO_ADICIONAL = 'SEGURO_ADICIONAL',
|
||||||
|
OTRO = 'OTRO',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'billing', name: 'recargos_catalogo' })
|
||||||
|
@Index('idx_recargo_tenant', ['tenantId'])
|
||||||
|
@Index('idx_recargo_codigo', ['tenantId', 'codigo'], { unique: true })
|
||||||
|
export class RecargoCatalogo {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Identificación
|
||||||
|
@Column({ type: 'varchar', length: 50 })
|
||||||
|
codigo: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 200 })
|
||||||
|
nombre: string;
|
||||||
|
|
||||||
|
@Column({ type: 'enum', enum: TipoRecargo })
|
||||||
|
tipo: TipoRecargo;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
descripcion: string | null;
|
||||||
|
|
||||||
|
// Monto
|
||||||
|
@Column({ name: 'es_porcentaje', type: 'boolean', default: false })
|
||||||
|
esPorcentaje: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 15, scale: 4 })
|
||||||
|
monto: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||||
|
moneda: string;
|
||||||
|
|
||||||
|
// Aplicación
|
||||||
|
@Column({ name: 'aplica_automatico', type: 'boolean', default: false })
|
||||||
|
aplicaAutomatico: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'condicion_aplicacion', type: 'text', nullable: true })
|
||||||
|
condicionAplicacion: string | null;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
activo: boolean;
|
||||||
|
|
||||||
|
// Auditoría
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'created_by_id', type: 'uuid' })
|
||||||
|
createdById: string;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
calcularRecargo(base: number): number {
|
||||||
|
if (this.esPorcentaje) {
|
||||||
|
return base * (this.monto / 100);
|
||||||
|
}
|
||||||
|
return this.monto;
|
||||||
|
}
|
||||||
|
|
||||||
|
get descripcionMonto(): string {
|
||||||
|
if (this.esPorcentaje) {
|
||||||
|
return `${this.monto}%`;
|
||||||
|
}
|
||||||
|
return `$${this.monto.toFixed(2)} ${this.moneda}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
161
src/modules/tarifas-transporte/entities/tarifa.entity.ts
Normal file
161
src/modules/tarifas-transporte/entities/tarifa.entity.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Lane } from './lane.entity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo de Tarifa
|
||||||
|
*/
|
||||||
|
export enum TipoTarifa {
|
||||||
|
POR_VIAJE = 'POR_VIAJE',
|
||||||
|
POR_KM = 'POR_KM',
|
||||||
|
POR_TONELADA = 'POR_TONELADA',
|
||||||
|
POR_M3 = 'POR_M3',
|
||||||
|
POR_PALLET = 'POR_PALLET',
|
||||||
|
POR_HORA = 'POR_HORA',
|
||||||
|
MIXTA = 'MIXTA',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ schema: 'billing', name: 'tarifas' })
|
||||||
|
@Index('idx_tarifa_tenant', ['tenantId'])
|
||||||
|
@Index('idx_tarifa_cliente', ['clienteId'])
|
||||||
|
@Index('idx_tarifa_lane', ['laneId'])
|
||||||
|
@Index('idx_tarifa_activa', ['tenantId', 'activa', 'fechaInicio'])
|
||||||
|
export class Tarifa {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
// Identificación
|
||||||
|
@Column({ type: 'varchar', length: 50 })
|
||||||
|
codigo: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 200 })
|
||||||
|
nombre: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
descripcion: string | null;
|
||||||
|
|
||||||
|
// Cliente (opcional para tarifas generales)
|
||||||
|
@Column({ name: 'cliente_id', type: 'uuid', nullable: true })
|
||||||
|
clienteId: string | null;
|
||||||
|
|
||||||
|
// Lane (opcional)
|
||||||
|
@Column({ name: 'lane_id', type: 'uuid', nullable: true })
|
||||||
|
laneId: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => Lane)
|
||||||
|
@JoinColumn({ name: 'lane_id' })
|
||||||
|
lane: Lane | null;
|
||||||
|
|
||||||
|
// Tipo de servicio
|
||||||
|
@Column({ name: 'modalidad_servicio', type: 'varchar', length: 50, nullable: true })
|
||||||
|
modalidadServicio: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'tipo_equipo', type: 'varchar', length: 50, nullable: true })
|
||||||
|
tipoEquipo: string | null;
|
||||||
|
|
||||||
|
// Tipo de tarifa
|
||||||
|
@Column({ name: 'tipo_tarifa', type: 'enum', enum: TipoTarifa })
|
||||||
|
tipoTarifa: TipoTarifa;
|
||||||
|
|
||||||
|
// Precios
|
||||||
|
@Column({ name: 'tarifa_base', type: 'decimal', precision: 15, scale: 2 })
|
||||||
|
tarifaBase: number;
|
||||||
|
|
||||||
|
@Column({ name: 'tarifa_km', type: 'decimal', precision: 10, scale: 4, nullable: true })
|
||||||
|
tarifaKm: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'tarifa_tonelada', type: 'decimal', precision: 10, scale: 4, nullable: true })
|
||||||
|
tarifaTonelada: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'tarifa_m3', type: 'decimal', precision: 10, scale: 4, nullable: true })
|
||||||
|
tarifaM3: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'tarifa_pallet', type: 'decimal', precision: 10, scale: 4, nullable: true })
|
||||||
|
tarifaPallet: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'tarifa_hora', type: 'decimal', precision: 10, scale: 4, nullable: true })
|
||||||
|
tarifaHora: number | null;
|
||||||
|
|
||||||
|
// Mínimos
|
||||||
|
@Column({ name: 'minimo_facturar', type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||||
|
minimoFacturar: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'peso_minimo_kg', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||||
|
pesoMinimoKg: number | null;
|
||||||
|
|
||||||
|
// Moneda
|
||||||
|
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||||
|
moneda: string;
|
||||||
|
|
||||||
|
// Vigencia
|
||||||
|
@Column({ name: 'fecha_inicio', type: 'date' })
|
||||||
|
fechaInicio: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'fecha_fin', type: 'date', nullable: true })
|
||||||
|
fechaFin: Date | null;
|
||||||
|
|
||||||
|
// Estado
|
||||||
|
@Column({ type: 'boolean', default: true })
|
||||||
|
activa: boolean;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
get vigente(): boolean {
|
||||||
|
const hoy = new Date();
|
||||||
|
const inicio = new Date(this.fechaInicio);
|
||||||
|
const fin = this.fechaFin ? new Date(this.fechaFin) : null;
|
||||||
|
return this.activa && hoy >= inicio && (!fin || hoy <= fin);
|
||||||
|
}
|
||||||
|
|
||||||
|
get esEspecifica(): boolean {
|
||||||
|
return !!this.clienteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
calcularMonto(params: { km?: number; toneladas?: number; m3?: number; pallets?: number; horas?: number }): number {
|
||||||
|
let total = this.tarifaBase;
|
||||||
|
|
||||||
|
switch (this.tipoTarifa) {
|
||||||
|
case TipoTarifa.POR_KM:
|
||||||
|
total += (params.km || 0) * (this.tarifaKm || 0);
|
||||||
|
break;
|
||||||
|
case TipoTarifa.POR_TONELADA:
|
||||||
|
total += (params.toneladas || 0) * (this.tarifaTonelada || 0);
|
||||||
|
break;
|
||||||
|
case TipoTarifa.POR_M3:
|
||||||
|
total += (params.m3 || 0) * (this.tarifaM3 || 0);
|
||||||
|
break;
|
||||||
|
case TipoTarifa.POR_PALLET:
|
||||||
|
total += (params.pallets || 0) * (this.tarifaPallet || 0);
|
||||||
|
break;
|
||||||
|
case TipoTarifa.POR_HORA:
|
||||||
|
total += (params.horas || 0) * (this.tarifaHora || 0);
|
||||||
|
break;
|
||||||
|
case TipoTarifa.MIXTA:
|
||||||
|
total += (params.km || 0) * (this.tarifaKm || 0);
|
||||||
|
total += (params.toneladas || 0) * (this.tarifaTonelada || 0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.minimoFacturar ? Math.max(total, this.minimoFacturar) : total;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user