feat(carta-porte): Add CFDI Carta Porte 3.1 compliance entities
- CartaPorte: Main CFDI entity with emisor/receptor, totals, XML/PDF - UbicacionCartaPorte: Origin/destination locations with SAT codes - MercanciaCartaPorte: Goods being transported with hazmat support - FiguraTransporte: Operator/owner/lessor figures - AutotransporteCartaPorte: Vehicle configuration and trailers - HosLog: Hours of Service compliance (NOM-087) - InspeccionPreViaje: Pre-trip inspection checklists Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
2722920e12
commit
2120d6e8b0
@ -0,0 +1,77 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { CartaPorte } from './carta-porte.entity';
|
||||
|
||||
/**
|
||||
* Remolque para Carta Porte
|
||||
*/
|
||||
export interface RemolqueCartaPorte {
|
||||
subTipoRem: string; // Clave SAT del subtipo de remolque
|
||||
placa: string;
|
||||
}
|
||||
|
||||
@Entity({ schema: 'compliance', name: 'autotransporte_carta_porte' })
|
||||
@Index('idx_autotransporte_carta', ['cartaPorteId'])
|
||||
export class AutotransporteCartaPorte {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'carta_porte_id', type: 'uuid' })
|
||||
cartaPorteId: string;
|
||||
|
||||
@ManyToOne(() => CartaPorte, (cp) => cp.autotransporte)
|
||||
@JoinColumn({ name: 'carta_porte_id' })
|
||||
cartaPorte: CartaPorte;
|
||||
|
||||
// Permiso
|
||||
@Column({ name: 'perm_sct', type: 'varchar', length: 10 })
|
||||
permSct: string;
|
||||
|
||||
@Column({ name: 'num_permiso_sct', type: 'varchar', length: 50 })
|
||||
numPermisoSct: string;
|
||||
|
||||
// Identificación vehicular tractora
|
||||
@Column({ name: 'config_vehicular', type: 'varchar', length: 10 })
|
||||
configVehicular: string;
|
||||
|
||||
@Column({ name: 'placa_vm', type: 'varchar', length: 15 })
|
||||
placaVm: string;
|
||||
|
||||
@Column({ name: 'anio_modelo_vm', type: 'int', nullable: true })
|
||||
anioModeloVm: number | null;
|
||||
|
||||
// Remolques
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
remolques: RemolqueCartaPorte[] | null;
|
||||
|
||||
// Helpers
|
||||
get tieneRemolques(): boolean {
|
||||
return Array.isArray(this.remolques) && this.remolques.length > 0;
|
||||
}
|
||||
|
||||
get cantidadRemolques(): number {
|
||||
return this.remolques?.length || 0;
|
||||
}
|
||||
|
||||
get placasRemolques(): string[] {
|
||||
return this.remolques?.map(r => r.placa) || [];
|
||||
}
|
||||
|
||||
get configCompleta(): string {
|
||||
const base = `${this.configVehicular} (${this.placaVm})`;
|
||||
if (this.tieneRemolques) {
|
||||
const remolquesStr = this.placasRemolques.join(', ');
|
||||
return `${base} + Remolques: ${remolquesStr}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
}
|
||||
220
src/modules/carta-porte/entities/carta-porte.entity.ts
Normal file
220
src/modules/carta-porte/entities/carta-porte.entity.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { UbicacionCartaPorte } from './ubicacion-carta-porte.entity';
|
||||
import { MercanciaCartaPorte } from './mercancia-carta-porte.entity';
|
||||
import { FiguraTransporte } from './figura-transporte.entity';
|
||||
import { AutotransporteCartaPorte } from './autotransporte-carta-porte.entity';
|
||||
|
||||
/**
|
||||
* Estado de Carta Porte
|
||||
*/
|
||||
export enum EstadoCartaPorte {
|
||||
BORRADOR = 'BORRADOR',
|
||||
VALIDADA = 'VALIDADA',
|
||||
TIMBRADA = 'TIMBRADA',
|
||||
CANCELADA = 'CANCELADA',
|
||||
}
|
||||
|
||||
/**
|
||||
* Tipo de CFDI para Carta Porte
|
||||
*/
|
||||
export enum TipoCfdiCartaPorte {
|
||||
INGRESO = 'INGRESO', // Servicio de transporte (factura)
|
||||
TRASLADO = 'TRASLADO', // Traslado propio (sin cobro)
|
||||
}
|
||||
|
||||
@Entity({ schema: 'compliance', name: 'cartas_porte' })
|
||||
@Index('idx_carta_porte_tenant', ['tenantId'])
|
||||
@Index('idx_carta_porte_viaje', ['viajeId'])
|
||||
@Index('idx_carta_porte_uuid', ['uuidCfdi'])
|
||||
@Index('idx_carta_porte_estado', ['tenantId', 'estado'])
|
||||
export class CartaPorte {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
// Viaje relacionado
|
||||
@Column({ name: 'viaje_id', type: 'uuid' })
|
||||
viajeId: string;
|
||||
|
||||
// CFDI
|
||||
@Column({ name: 'tipo_cfdi', type: 'enum', enum: TipoCfdiCartaPorte })
|
||||
tipoCfdi: TipoCfdiCartaPorte;
|
||||
|
||||
@Column({ name: 'version_carta_porte', type: 'varchar', length: 10, default: '3.1' })
|
||||
versionCartaPorte: string;
|
||||
|
||||
// Identificación CFDI
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
serie: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, nullable: true })
|
||||
folio: string | null;
|
||||
|
||||
@Column({ name: 'uuid_cfdi', type: 'uuid', nullable: true })
|
||||
uuidCfdi: string | null;
|
||||
|
||||
@Column({ name: 'fecha_timbrado', type: 'timestamptz', nullable: true })
|
||||
fechaTimbrado: Date | null;
|
||||
|
||||
// Emisor
|
||||
@Column({ name: 'emisor_rfc', type: 'varchar', length: 13 })
|
||||
emisorRfc: string;
|
||||
|
||||
@Column({ name: 'emisor_nombre', type: 'varchar', length: 200 })
|
||||
emisorNombre: string;
|
||||
|
||||
@Column({ name: 'emisor_regimen_fiscal', type: 'varchar', length: 10, nullable: true })
|
||||
emisorRegimenFiscal: string | null;
|
||||
|
||||
// Receptor
|
||||
@Column({ name: 'receptor_rfc', type: 'varchar', length: 13 })
|
||||
receptorRfc: string;
|
||||
|
||||
@Column({ name: 'receptor_nombre', type: 'varchar', length: 200 })
|
||||
receptorNombre: string;
|
||||
|
||||
@Column({ name: 'receptor_uso_cfdi', type: 'varchar', length: 10, nullable: true })
|
||||
receptorUsoCfdi: string | null;
|
||||
|
||||
@Column({ name: 'receptor_domicilio_fiscal_cp', type: 'varchar', length: 10, nullable: true })
|
||||
receptorDomicilioFiscalCp: string | null;
|
||||
|
||||
// Totales CFDI
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||
subtotal: number | null;
|
||||
|
||||
@Column({ type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||
total: number | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||
moneda: string;
|
||||
|
||||
// Datos transporte federal
|
||||
@Column({ name: 'transporte_internacional', type: 'boolean', default: false })
|
||||
transporteInternacional: boolean;
|
||||
|
||||
@Column({ name: 'entrada_salida_merc', type: 'varchar', length: 10, nullable: true })
|
||||
entradaSalidaMerc: string | null;
|
||||
|
||||
@Column({ name: 'pais_origen_destino', type: 'varchar', length: 3, nullable: true })
|
||||
paisOrigenDestino: string | null;
|
||||
|
||||
// Datos autotransporte
|
||||
@Column({ name: 'permiso_sct', type: 'varchar', length: 50, nullable: true })
|
||||
permisoSct: string | null;
|
||||
|
||||
@Column({ name: 'num_permiso_sct', type: 'varchar', length: 50, nullable: true })
|
||||
numPermisoSct: string | null;
|
||||
|
||||
@Column({ name: 'config_vehicular', type: 'varchar', length: 10, nullable: true })
|
||||
configVehicular: string | null;
|
||||
|
||||
@Column({ name: 'peso_bruto_total', type: 'decimal', precision: 12, scale: 3, nullable: true })
|
||||
pesoBrutoTotal: number | null;
|
||||
|
||||
@Column({ name: 'unidad_peso', type: 'varchar', length: 10, default: 'KGM' })
|
||||
unidadPeso: string;
|
||||
|
||||
@Column({ name: 'num_total_mercancias', type: 'int', nullable: true })
|
||||
numTotalMercancias: number | null;
|
||||
|
||||
// Seguro
|
||||
@Column({ name: 'asegura_resp_civil', type: 'varchar', length: 100, nullable: true })
|
||||
aseguraRespCivil: string | null;
|
||||
|
||||
@Column({ name: 'poliza_resp_civil', type: 'varchar', length: 50, nullable: true })
|
||||
polizaRespCivil: string | null;
|
||||
|
||||
@Column({ name: 'asegura_med_ambiente', type: 'varchar', length: 100, nullable: true })
|
||||
aseguraMedAmbiente: string | null;
|
||||
|
||||
@Column({ name: 'poliza_med_ambiente', type: 'varchar', length: 50, nullable: true })
|
||||
polizaMedAmbiente: string | null;
|
||||
|
||||
@Column({ name: 'asegura_carga', type: 'varchar', length: 100, nullable: true })
|
||||
aseguraCarga: string | null;
|
||||
|
||||
@Column({ name: 'poliza_carga', type: 'varchar', length: 50, nullable: true })
|
||||
polizaCarga: string | null;
|
||||
|
||||
@Column({ name: 'prima_seguro', type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||
primaSeguro: number | null;
|
||||
|
||||
// Estado
|
||||
@Column({ type: 'enum', enum: EstadoCartaPorte, default: EstadoCartaPorte.BORRADOR })
|
||||
estado: EstadoCartaPorte;
|
||||
|
||||
// XML y PDF
|
||||
@Column({ name: 'xml_cfdi', type: 'text', nullable: true })
|
||||
xmlCfdi: string | null;
|
||||
|
||||
@Column({ name: 'xml_carta_porte', type: 'text', nullable: true })
|
||||
xmlCartaPorte: string | null;
|
||||
|
||||
@Column({ name: 'pdf_url', type: 'text', nullable: true })
|
||||
pdfUrl: string | null;
|
||||
|
||||
@Column({ name: 'qr_url', type: 'text', nullable: true })
|
||||
qrUrl: string | 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;
|
||||
|
||||
@Column({ name: 'uuid_sustitucion', type: 'uuid', nullable: true })
|
||||
uuidSustitucion: string | null;
|
||||
|
||||
// Relations
|
||||
@OneToMany(() => UbicacionCartaPorte, (ubicacion) => ubicacion.cartaPorte)
|
||||
ubicaciones: UbicacionCartaPorte[];
|
||||
|
||||
@OneToMany(() => MercanciaCartaPorte, (mercancia) => mercancia.cartaPorte)
|
||||
mercancias: MercanciaCartaPorte[];
|
||||
|
||||
@OneToMany(() => FiguraTransporte, (figura) => figura.cartaPorte)
|
||||
figuras: FiguraTransporte[];
|
||||
|
||||
@OneToMany(() => AutotransporteCartaPorte, (auto) => auto.cartaPorte)
|
||||
autotransporte: AutotransporteCartaPorte[];
|
||||
|
||||
// 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 estaTimbrada(): boolean {
|
||||
return this.estado === EstadoCartaPorte.TIMBRADA && !!this.uuidCfdi;
|
||||
}
|
||||
|
||||
get estaCancelada(): boolean {
|
||||
return this.estado === EstadoCartaPorte.CANCELADA;
|
||||
}
|
||||
|
||||
get folioCompleto(): string {
|
||||
if (!this.folio) return '';
|
||||
return this.serie ? `${this.serie}-${this.folio}` : this.folio;
|
||||
}
|
||||
|
||||
get esIngreso(): boolean {
|
||||
return this.tipoCfdi === TipoCfdiCartaPorte.INGRESO;
|
||||
}
|
||||
}
|
||||
97
src/modules/carta-porte/entities/figura-transporte.entity.ts
Normal file
97
src/modules/carta-porte/entities/figura-transporte.entity.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { CartaPorte } from './carta-porte.entity';
|
||||
|
||||
/**
|
||||
* Tipo de Figura de Transporte (catálogo SAT)
|
||||
*/
|
||||
export enum TipoFigura {
|
||||
OPERADOR = '01', // Operador/Conductor
|
||||
PROPIETARIO = '02', // Propietario de la mercancía
|
||||
ARRENDADOR = '03', // Arrendador del vehículo
|
||||
NOTIFICADO = '04', // Notificado
|
||||
}
|
||||
|
||||
/**
|
||||
* Partes de Transporte
|
||||
*/
|
||||
export interface ParteTransporte {
|
||||
parteTransporte: string;
|
||||
}
|
||||
|
||||
@Entity({ schema: 'compliance', name: 'figuras_transporte' })
|
||||
@Index('idx_figura_carta', ['cartaPorteId'])
|
||||
export class FiguraTransporte {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'carta_porte_id', type: 'uuid' })
|
||||
cartaPorteId: string;
|
||||
|
||||
@ManyToOne(() => CartaPorte, (cp) => cp.figuras)
|
||||
@JoinColumn({ name: 'carta_porte_id' })
|
||||
cartaPorte: CartaPorte;
|
||||
|
||||
// Tipo de figura
|
||||
@Column({ name: 'tipo_figura', type: 'varchar', length: 10 })
|
||||
tipoFigura: string;
|
||||
|
||||
// Datos
|
||||
@Column({ name: 'rfc_figura', type: 'varchar', length: 13, nullable: true })
|
||||
rfcFigura: string | null;
|
||||
|
||||
@Column({ name: 'nombre_figura', type: 'varchar', length: 200, nullable: true })
|
||||
nombreFigura: string | null;
|
||||
|
||||
@Column({ name: 'num_licencia', type: 'varchar', length: 50, nullable: true })
|
||||
numLicencia: string | null;
|
||||
|
||||
// Domicilio (opcional)
|
||||
@Column({ type: 'varchar', length: 3, nullable: true })
|
||||
pais: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
estado: string | null;
|
||||
|
||||
@Column({ name: 'codigo_postal', type: 'varchar', length: 10, nullable: true })
|
||||
codigoPostal: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 200, nullable: true })
|
||||
calle: string | null;
|
||||
|
||||
// Partes transporte (solo para Propietario/Arrendador)
|
||||
@Column({ name: 'partes_transporte', type: 'jsonb', nullable: true })
|
||||
partesTransporte: ParteTransporte[] | null;
|
||||
|
||||
// Helpers
|
||||
get esOperador(): boolean {
|
||||
return this.tipoFigura === TipoFigura.OPERADOR;
|
||||
}
|
||||
|
||||
get esPropietario(): boolean {
|
||||
return this.tipoFigura === TipoFigura.PROPIETARIO;
|
||||
}
|
||||
|
||||
get esArrendador(): boolean {
|
||||
return this.tipoFigura === TipoFigura.ARRENDADOR;
|
||||
}
|
||||
|
||||
get tipoFiguraDescripcion(): string {
|
||||
switch (this.tipoFigura) {
|
||||
case TipoFigura.OPERADOR: return 'Operador';
|
||||
case TipoFigura.PROPIETARIO: return 'Propietario';
|
||||
case TipoFigura.ARRENDADOR: return 'Arrendador';
|
||||
case TipoFigura.NOTIFICADO: return 'Notificado';
|
||||
default: return 'Desconocido';
|
||||
}
|
||||
}
|
||||
}
|
||||
106
src/modules/carta-porte/entities/hos-log.entity.ts
Normal file
106
src/modules/carta-porte/entities/hos-log.entity.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* Estado HOS (Hours of Service)
|
||||
* NOM-087-SCT-2-2017
|
||||
*/
|
||||
export enum EstadoHos {
|
||||
DRIVING = 'DRIVING', // Conduciendo
|
||||
ON_DUTY = 'ON_DUTY', // En servicio (no conduciendo)
|
||||
SLEEPER = 'SLEEPER', // En litera
|
||||
OFF_DUTY = 'OFF_DUTY', // Fuera de servicio
|
||||
}
|
||||
|
||||
@Entity({ schema: 'compliance', name: 'hos_logs' })
|
||||
@Index('idx_hos_operador', ['operadorId'])
|
||||
@Index('idx_hos_fecha', ['tenantId', 'fecha'])
|
||||
@Index('idx_hos_viaje', ['viajeId'])
|
||||
export class HosLog {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
// Operador y viaje
|
||||
@Column({ name: 'operador_id', type: 'uuid' })
|
||||
operadorId: string;
|
||||
|
||||
@Column({ name: 'viaje_id', type: 'uuid', nullable: true })
|
||||
viajeId: string | null;
|
||||
|
||||
// Log
|
||||
@Column({ type: 'date' })
|
||||
fecha: Date;
|
||||
|
||||
@Column({ name: 'hora_inicio', type: 'time' })
|
||||
horaInicio: string;
|
||||
|
||||
@Column({ name: 'hora_fin', type: 'time', nullable: true })
|
||||
horaFin: string | null;
|
||||
|
||||
@Column({ name: 'duracion_minutos', type: 'int', nullable: true })
|
||||
duracionMinutos: number | null;
|
||||
|
||||
// Estado
|
||||
@Column({ type: 'enum', enum: EstadoHos })
|
||||
estado: EstadoHos;
|
||||
|
||||
// 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;
|
||||
|
||||
@Column({ name: 'ubicacion_descripcion', type: 'varchar', length: 200, nullable: true })
|
||||
ubicacionDescripcion: string | null;
|
||||
|
||||
// Odómetro
|
||||
@Column({ name: 'odometro_inicio', type: 'int', nullable: true })
|
||||
odometroInicio: number | null;
|
||||
|
||||
@Column({ name: 'odometro_fin', type: 'int', nullable: true })
|
||||
odometroFin: number | null;
|
||||
|
||||
// Observaciones
|
||||
@Column({ type: 'text', nullable: true })
|
||||
observaciones: string | null;
|
||||
|
||||
// Certificado
|
||||
@Column({ name: 'certificado_por_operador', type: 'boolean', default: false })
|
||||
certificadoPorOperador: boolean;
|
||||
|
||||
@Column({ name: 'certificado_fecha', type: 'timestamptz', nullable: true })
|
||||
certificadoFecha: Date | null;
|
||||
|
||||
// Auditoría
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
// Helpers
|
||||
get duracionHoras(): number {
|
||||
return (this.duracionMinutos || 0) / 60;
|
||||
}
|
||||
|
||||
get kmRecorridos(): number | null {
|
||||
if (this.odometroInicio && this.odometroFin) {
|
||||
return this.odometroFin - this.odometroInicio;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get esConduccion(): boolean {
|
||||
return this.estado === EstadoHos.DRIVING;
|
||||
}
|
||||
|
||||
get esDescanso(): boolean {
|
||||
return this.estado === EstadoHos.OFF_DUTY || this.estado === EstadoHos.SLEEPER;
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,12 @@
|
||||
/**
|
||||
* Carta Porte Entities
|
||||
* Schema: compliance
|
||||
* CFDI con Complemento Carta Porte 3.1
|
||||
*/
|
||||
// TODO: Implement entities
|
||||
// - carta-porte.entity.ts
|
||||
// - ubicacion-carta-porte.entity.ts
|
||||
// - mercancia-carta-porte.entity.ts
|
||||
// - autotransporte-carta-porte.entity.ts
|
||||
// - figura-transporte.entity.ts
|
||||
export * from './carta-porte.entity';
|
||||
export * from './ubicacion-carta-porte.entity';
|
||||
export * from './mercancia-carta-porte.entity';
|
||||
export * from './figura-transporte.entity';
|
||||
export * from './autotransporte-carta-porte.entity';
|
||||
export * from './hos-log.entity';
|
||||
export * from './inspeccion-pre-viaje.entity';
|
||||
|
||||
117
src/modules/carta-porte/entities/inspeccion-pre-viaje.entity.ts
Normal file
117
src/modules/carta-porte/entities/inspeccion-pre-viaje.entity.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
/**
|
||||
* Item del Checklist de Inspección
|
||||
*/
|
||||
export interface ChecklistItem {
|
||||
item: string;
|
||||
categoria: string;
|
||||
estado: 'OK' | 'DEFECTO_MENOR' | 'DEFECTO_CRITICO' | 'NO_APLICA';
|
||||
observacion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Foto de Inspección
|
||||
*/
|
||||
export interface FotoInspeccion {
|
||||
url: string;
|
||||
descripcion?: string;
|
||||
categoria?: string;
|
||||
}
|
||||
|
||||
@Entity({ schema: 'compliance', name: 'inspecciones_pre_viaje' })
|
||||
@Index('idx_inspeccion_viaje', ['viajeId'])
|
||||
@Index('idx_inspeccion_unidad', ['unidadId'])
|
||||
@Index('idx_inspeccion_fecha', ['tenantId', 'fechaInspeccion'])
|
||||
export class InspeccionPreViaje {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
// Viaje y unidad
|
||||
@Column({ name: 'viaje_id', type: 'uuid' })
|
||||
viajeId: string;
|
||||
|
||||
@Column({ name: 'unidad_id', type: 'uuid' })
|
||||
unidadId: string;
|
||||
|
||||
@Column({ name: 'remolque_id', type: 'uuid', nullable: true })
|
||||
remolqueId: string | null;
|
||||
|
||||
@Column({ name: 'operador_id', type: 'uuid' })
|
||||
operadorId: string;
|
||||
|
||||
// Fecha
|
||||
@Column({ name: 'fecha_inspeccion', type: 'timestamptz' })
|
||||
fechaInspeccion: Date;
|
||||
|
||||
// Resultado general
|
||||
@Column({ type: 'boolean', default: false })
|
||||
aprobada: boolean;
|
||||
|
||||
// Items checklist
|
||||
@Column({ name: 'checklist_items', type: 'jsonb' })
|
||||
checklistItems: ChecklistItem[];
|
||||
|
||||
// Defectos encontrados
|
||||
@Column({ name: 'defectos_encontrados', type: 'text', array: true, nullable: true })
|
||||
defectosEncontrados: string[] | null;
|
||||
|
||||
@Column({ name: 'defectos_criticos', type: 'int', default: 0 })
|
||||
defectosCriticos: number;
|
||||
|
||||
@Column({ name: 'defectos_menores', type: 'int', default: 0 })
|
||||
defectosMenores: number;
|
||||
|
||||
// Firma
|
||||
@Column({ name: 'firma_operador', type: 'text', nullable: true })
|
||||
firmaOperador: string | null;
|
||||
|
||||
@Column({ name: 'firma_fecha', type: 'timestamptz', nullable: true })
|
||||
firmaFecha: Date | null;
|
||||
|
||||
// Evidencias
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
fotos: FotoInspeccion[] | null;
|
||||
|
||||
// Auditoría
|
||||
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
// Helpers
|
||||
get totalDefectos(): number {
|
||||
return this.defectosCriticos + this.defectosMenores;
|
||||
}
|
||||
|
||||
get tieneDefectosCriticos(): boolean {
|
||||
return this.defectosCriticos > 0;
|
||||
}
|
||||
|
||||
get puedeViajar(): boolean {
|
||||
return this.aprobada && this.defectosCriticos === 0;
|
||||
}
|
||||
|
||||
get tieneFirma(): boolean {
|
||||
return !!this.firmaOperador;
|
||||
}
|
||||
|
||||
get itemsConDefecto(): ChecklistItem[] {
|
||||
return this.checklistItems.filter(
|
||||
item => item.estado === 'DEFECTO_MENOR' || item.estado === 'DEFECTO_CRITICO'
|
||||
);
|
||||
}
|
||||
|
||||
get porcentajeAprobacion(): number {
|
||||
const total = this.checklistItems.filter(i => i.estado !== 'NO_APLICA').length;
|
||||
const aprobados = this.checklistItems.filter(i => i.estado === 'OK').length;
|
||||
return total > 0 ? (aprobados / total) * 100 : 0;
|
||||
}
|
||||
}
|
||||
108
src/modules/carta-porte/entities/mercancia-carta-porte.entity.ts
Normal file
108
src/modules/carta-porte/entities/mercancia-carta-porte.entity.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { CartaPorte } from './carta-porte.entity';
|
||||
|
||||
@Entity({ schema: 'compliance', name: 'mercancias_carta_porte' })
|
||||
@Index('idx_mercancia_carta', ['cartaPorteId'])
|
||||
export class MercanciaCartaPorte {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'carta_porte_id', type: 'uuid' })
|
||||
cartaPorteId: string;
|
||||
|
||||
@ManyToOne(() => CartaPorte, (cp) => cp.mercancias)
|
||||
@JoinColumn({ name: 'carta_porte_id' })
|
||||
cartaPorte: CartaPorte;
|
||||
|
||||
// Bienes transportados
|
||||
@Column({ name: 'bienes_transp', type: 'varchar', length: 10 })
|
||||
bienesTransp: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 1000 })
|
||||
descripcion: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 14, scale: 3 })
|
||||
cantidad: number;
|
||||
|
||||
@Column({ name: 'clave_unidad', type: 'varchar', length: 10 })
|
||||
claveUnidad: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, nullable: true })
|
||||
unidad: string | null;
|
||||
|
||||
// Dimensiones
|
||||
@Column({ name: 'peso_en_kg', type: 'decimal', precision: 14, scale: 3 })
|
||||
pesoEnKg: number;
|
||||
|
||||
@Column({ name: 'largo_cm', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
largoCm: number | null;
|
||||
|
||||
@Column({ name: 'ancho_cm', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
anchoCm: number | null;
|
||||
|
||||
@Column({ name: 'alto_cm', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
altoCm: number | null;
|
||||
|
||||
// Valor
|
||||
@Column({ name: 'valor_mercancia', type: 'decimal', precision: 15, scale: 2, nullable: true })
|
||||
valorMercancia: number | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||
moneda: string;
|
||||
|
||||
// Material peligroso
|
||||
@Column({ name: 'material_peligroso', type: 'boolean', default: false })
|
||||
materialPeligroso: boolean;
|
||||
|
||||
@Column({ name: 'cve_material_peligroso', type: 'varchar', length: 10, nullable: true })
|
||||
cveMaterialPeligroso: string | null;
|
||||
|
||||
@Column({ name: 'tipo_embalaje', type: 'varchar', length: 10, nullable: true })
|
||||
tipoEmbalaje: string | null;
|
||||
|
||||
@Column({ name: 'descripcion_embalaje', type: 'varchar', length: 200, nullable: true })
|
||||
descripcionEmbalaje: string | null;
|
||||
|
||||
// Fracción arancelaria (comercio exterior)
|
||||
@Column({ name: 'fraccion_arancelaria', type: 'varchar', length: 10, nullable: true })
|
||||
fraccionArancelaria: string | null;
|
||||
|
||||
@Column({ name: 'uuid_comercio_ext', type: 'uuid', nullable: true })
|
||||
uuidComercioExt: string | null;
|
||||
|
||||
// Pedimentos
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
pedimentos: string[] | null;
|
||||
|
||||
// Guías
|
||||
@Column({ type: 'text', array: true, nullable: true })
|
||||
guias: string[] | null;
|
||||
|
||||
// Secuencia
|
||||
@Column({ type: 'int' })
|
||||
secuencia: number;
|
||||
|
||||
// Helpers
|
||||
get esPeligroso(): boolean {
|
||||
return this.materialPeligroso;
|
||||
}
|
||||
|
||||
get volumenM3(): number | null {
|
||||
if (!this.largoCm || !this.anchoCm || !this.altoCm) return null;
|
||||
return (this.largoCm * this.anchoCm * this.altoCm) / 1000000; // cm³ to m³
|
||||
}
|
||||
|
||||
get tieneComercioExterior(): boolean {
|
||||
return !!this.fraccionArancelaria || !!this.uuidComercioExt;
|
||||
}
|
||||
}
|
||||
112
src/modules/carta-porte/entities/ubicacion-carta-porte.entity.ts
Normal file
112
src/modules/carta-porte/entities/ubicacion-carta-porte.entity.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
Index,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { CartaPorte } from './carta-porte.entity';
|
||||
|
||||
/**
|
||||
* Tipo de Ubicación
|
||||
*/
|
||||
export enum TipoUbicacionCartaPorte {
|
||||
ORIGEN = 'Origen',
|
||||
DESTINO = 'Destino',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'compliance', name: 'ubicaciones_carta_porte' })
|
||||
@Index('idx_ubicacion_carta', ['cartaPorteId'])
|
||||
export class UbicacionCartaPorte {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'carta_porte_id', type: 'uuid' })
|
||||
cartaPorteId: string;
|
||||
|
||||
@ManyToOne(() => CartaPorte, (cp) => cp.ubicaciones)
|
||||
@JoinColumn({ name: 'carta_porte_id' })
|
||||
cartaPorte: CartaPorte;
|
||||
|
||||
// Tipo
|
||||
@Column({ name: 'tipo_ubicacion', type: 'varchar', length: 10 })
|
||||
tipoUbicacion: string;
|
||||
|
||||
// ID Ubicación (catálogo SAT)
|
||||
@Column({ name: 'id_ubicacion', type: 'varchar', length: 10, nullable: true })
|
||||
idUbicacion: string | null;
|
||||
|
||||
// RFC
|
||||
@Column({ name: 'rfc_remitente_destinatario', type: 'varchar', length: 13, nullable: true })
|
||||
rfcRemitenteDestinatario: string | null;
|
||||
|
||||
@Column({ name: 'nombre_remitente_destinatario', type: 'varchar', length: 200, nullable: true })
|
||||
nombreRemitenteDestinatario: string | null;
|
||||
|
||||
// Domicilio
|
||||
@Column({ type: 'varchar', length: 3, default: 'MEX' })
|
||||
pais: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
estado: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
municipio: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
localidad: string | null;
|
||||
|
||||
@Column({ name: 'codigo_postal', type: 'varchar', length: 10 })
|
||||
codigoPostal: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 10, nullable: true })
|
||||
colonia: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 200, nullable: true })
|
||||
calle: string | null;
|
||||
|
||||
@Column({ name: 'numero_exterior', type: 'varchar', length: 50, nullable: true })
|
||||
numeroExterior: string | null;
|
||||
|
||||
@Column({ name: 'numero_interior', type: 'varchar', length: 50, nullable: true })
|
||||
numeroInterior: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 500, nullable: true })
|
||||
referencia: string | null;
|
||||
|
||||
// Fechas
|
||||
@Column({ name: 'fecha_hora_salida_llegada', type: 'timestamptz', nullable: true })
|
||||
fechaHoraSalidaLlegada: Date | null;
|
||||
|
||||
// Distancia
|
||||
@Column({ name: 'distancia_recorrida', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
distanciaRecorrida: number | null;
|
||||
|
||||
// Secuencia
|
||||
@Column({ type: 'int' })
|
||||
secuencia: number;
|
||||
|
||||
// Helpers
|
||||
get esOrigen(): boolean {
|
||||
return this.tipoUbicacion === TipoUbicacionCartaPorte.ORIGEN;
|
||||
}
|
||||
|
||||
get esDestino(): boolean {
|
||||
return this.tipoUbicacion === TipoUbicacionCartaPorte.DESTINO;
|
||||
}
|
||||
|
||||
get direccionCompleta(): string {
|
||||
const partes = [
|
||||
this.calle,
|
||||
this.numeroExterior ? `#${this.numeroExterior}` : null,
|
||||
this.numeroInterior ? `Int. ${this.numeroInterior}` : null,
|
||||
this.colonia,
|
||||
`CP ${this.codigoPostal}`,
|
||||
].filter(Boolean);
|
||||
return partes.join(', ');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user