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
|
* Carta Porte Entities
|
||||||
|
* Schema: compliance
|
||||||
|
* CFDI con Complemento Carta Porte 3.1
|
||||||
*/
|
*/
|
||||||
// TODO: Implement entities
|
export * from './carta-porte.entity';
|
||||||
// - carta-porte.entity.ts
|
export * from './ubicacion-carta-porte.entity';
|
||||||
// - ubicacion-carta-porte.entity.ts
|
export * from './mercancia-carta-porte.entity';
|
||||||
// - mercancia-carta-porte.entity.ts
|
export * from './figura-transporte.entity';
|
||||||
// - autotransporte-carta-porte.entity.ts
|
export * from './autotransporte-carta-porte.entity';
|
||||||
// - figura-transporte.entity.ts
|
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