feat(cfdi): agrega C.P. receptor, regimen receptor, no_identificacion, tipo_relacion y CFDIs relacionados al visualizador
Backend: - Migracion 044: codigo_postal_receptor VARCHAR(5) + indice - sat-parser: extrae DomicilioFiscalReceptor - sat.service: persiste codigo_postal_receptor en INSERT/UPDATE - cfdi.service: incluye codigo_postal_receptor en CFDI_SELECT - shared/types: codigoPostalReceptor en interfaz Cfdi Frontend: - cfdi-invoice: tarjeta receptor con C.P. y regimen (con descripciones) - cfdi-invoice: seccion CFDI Relacionado (tipo + UUIDs) - cfdi-invoice: columna No. Identificacion en tabla de conceptos - cfdi-viewer-modal: mapea noIdentificacion desde DB y XML
This commit is contained in:
2
apps/api/src/migrations/tenant/044_cfdi_cp_receptor.sql
Normal file
2
apps/api/src/migrations/tenant/044_cfdi_cp_receptor.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- Agregar código postal del receptor al CFDI (extraído del XML durante sync SAT)
|
||||
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS codigo_postal_receptor VARCHAR(5);
|
||||
@@ -60,6 +60,7 @@ const CFDI_SELECT = `
|
||||
conciliado,
|
||||
regimen_fiscal_emisor as "regimenFiscalEmisor",
|
||||
regimen_fiscal_receptor as "regimenFiscalReceptor",
|
||||
codigo_postal_receptor as "codigoPostalReceptor",
|
||||
xml_url as "xmlUrl", pdf_url as "pdfUrl",
|
||||
xml_original as "xmlOriginal",
|
||||
cfdi_tipo_relacion as "cfdiTipoRelacion",
|
||||
|
||||
@@ -61,6 +61,7 @@ interface CfdiParsed {
|
||||
|
||||
regimenFiscalEmisor: string | null;
|
||||
regimenFiscalReceptor: string | null;
|
||||
codigoPostalReceptor: string | null;
|
||||
// CfdiRelacionados a nivel raíz del comprobante (CFDI 4.0).
|
||||
// `cfdiTipoRelacion` — clave SAT (01..07). NULL si no hay relación.
|
||||
// `cfdisRelacionados` — UUIDs pipe-separated.
|
||||
@@ -541,6 +542,7 @@ export function parseXml(xmlContent: string, downloadType: 'emitidos' | 'recibid
|
||||
pac: timbreData.pac,
|
||||
regimenFiscalEmisor: emisor['@_RegimenFiscal'] || null,
|
||||
regimenFiscalReceptor: receptor['@_RegimenFiscalReceptor'] || receptor['@_RegimenFiscal'] || null,
|
||||
codigoPostalReceptor: receptor['@_DomicilioFiscalReceptor'] || null,
|
||||
cfdiTipoRelacion: relacionesData.tipoRelacion,
|
||||
cfdisRelacionados: relacionesData.uuids,
|
||||
// Impuestos comprobante
|
||||
|
||||
@@ -211,6 +211,7 @@ async function saveCfdis(
|
||||
cfdi.otrasDeduccionesNomina, m(cfdi.otrasDeduccionesNomina),
|
||||
cfdi.subsidioCausado, m(cfdi.subsidioCausado),
|
||||
cfdi.regimenFiscalEmisor, cfdi.regimenFiscalReceptor,
|
||||
cfdi.codigoPostalReceptor,
|
||||
cfdi.xmlOriginal,
|
||||
cfdi.cfdiTipoRelacion, cfdi.cfdisRelacionados,
|
||||
jobId,
|
||||
@@ -259,9 +260,10 @@ async function saveCfdis(
|
||||
otras_deducciones_nomina=$76, otras_deducciones_nomina_mxn=$77,
|
||||
subsidio_causado=$78, subsidio_causado_mxn=$79,
|
||||
regimen_fiscal_emisor=$80, regimen_fiscal_receptor=$81,
|
||||
xml_original=$82,
|
||||
cfdi_tipo_relacion=$83, cfdis_relacionados=$84,
|
||||
last_sat_sync=NOW(), sat_sync_job_id=$85::uuid,
|
||||
codigo_postal_receptor=$82,
|
||||
xml_original=$83,
|
||||
cfdi_tipo_relacion=$84, cfdis_relacionados=$85,
|
||||
last_sat_sync=NOW(), sat_sync_job_id=$86::uuid,
|
||||
actualizado_en=NOW()
|
||||
WHERE uuid = $1`,
|
||||
[cfdi.uuid, ...vals]
|
||||
@@ -307,6 +309,7 @@ async function saveCfdis(
|
||||
otras_deducciones_nomina, otras_deducciones_nomina_mxn,
|
||||
subsidio_causado, subsidio_causado_mxn,
|
||||
regimen_fiscal_emisor, regimen_fiscal_receptor,
|
||||
codigo_postal_receptor,
|
||||
xml_original,
|
||||
cfdi_tipo_relacion, cfdis_relacionados,
|
||||
source, sat_sync_job_id, last_sat_sync, contribuyente_id
|
||||
|
||||
@@ -10,6 +10,7 @@ interface CfdiConcepto {
|
||||
importe: number;
|
||||
claveUnidad?: string;
|
||||
claveProdServ?: string;
|
||||
noIdentificacion?: string;
|
||||
}
|
||||
|
||||
interface CfdiInvoiceProps {
|
||||
@@ -66,6 +67,38 @@ const metodoPagoLabels: Record<string, string> = {
|
||||
PPD: 'Pago en parcialidades o diferido',
|
||||
};
|
||||
|
||||
const regimenFiscalLabels: Record<string, string> = {
|
||||
'601': 'General de Ley Personas Morales',
|
||||
'603': 'Personas Morales con Fines no Lucrativos',
|
||||
'605': 'Sueldos y Salarios e Ingresos Asimilados a Salarios',
|
||||
'606': 'Arrendamiento',
|
||||
'608': 'Demás ingresos',
|
||||
'609': 'Consolidación',
|
||||
'610': 'Residentes en el Extranjero sin Establecimiento Permanente en México',
|
||||
'611': 'Ingresos por Dividendos (socios y accionistas)',
|
||||
'612': 'Personas Físicas con Actividades Empresariales y Profesionales',
|
||||
'614': 'Ingresos por intereses',
|
||||
'615': 'Régimen de los ingresos por obtención de premios',
|
||||
'616': 'Sin obligaciones fiscales',
|
||||
'620': 'Sociedades Cooperativas de Producción que optan por diferir sus ingresos',
|
||||
'621': 'Incorporación Fiscal',
|
||||
'622': 'Actividades Agrícolas, Ganaderas, Silvícolas y Pesqueras',
|
||||
'623': 'Opcional para Grupos de Sociedades',
|
||||
'624': 'Coordinados',
|
||||
'625': 'Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas',
|
||||
'626': 'Régimen Simplificado de Confianza',
|
||||
};
|
||||
|
||||
const tipoRelacionLabels: Record<string, string> = {
|
||||
'01': 'Nota de crédito',
|
||||
'02': 'Nota de débito',
|
||||
'03': 'Devolución de mercancía',
|
||||
'04': 'Sustitución de CFDI previo',
|
||||
'05': 'Traslados de mercancias facturados previamente',
|
||||
'06': 'Factura generada por traslados previos',
|
||||
'07': 'CFDI por aplicación de anticipo',
|
||||
};
|
||||
|
||||
const usoCfdiLabels: Record<string, string> = {
|
||||
G01: 'Adquisición de mercancías',
|
||||
G02: 'Devoluciones, descuentos o bonificaciones',
|
||||
@@ -142,13 +175,26 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
|
||||
{/* Receptor */}
|
||||
<div className="bg-gray-50 rounded-lg p-4 mb-5 border-l-4 border-blue-600">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Receptor</p>
|
||||
<p className="text-lg font-semibold text-gray-800 mt-1">{cfdi.nombreReceptor}</p>
|
||||
<p className="text-gray-600 text-sm">RFC: {cfdi.rfcReceptor}</p>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-1 text-sm text-gray-600">
|
||||
<span>RFC: {cfdi.rfcReceptor}</span>
|
||||
{cfdi.codigoPostalReceptor && (
|
||||
<span>C.P.: {cfdi.codigoPostalReceptor}</span>
|
||||
)}
|
||||
{cfdi.regimenFiscalReceptor && (
|
||||
<span>
|
||||
Régimen: {cfdi.regimenFiscalReceptor}
|
||||
{regimenFiscalLabels[cfdi.regimenFiscalReceptor]
|
||||
? ` — ${regimenFiscalLabels[cfdi.regimenFiscalReceptor]}`
|
||||
: ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{cfdi.usoCfdi && (
|
||||
<div className="text-right">
|
||||
<div className="text-right ml-4 flex-shrink-0">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wide font-medium">Uso CFDI</p>
|
||||
<p className="text-sm font-medium text-gray-700 mt-1">
|
||||
{cfdi.usoCfdi} - {usoCfdiLabels[cfdi.usoCfdi] || ''}
|
||||
@@ -158,6 +204,41 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CFDIs Relacionados */}
|
||||
{(cfdi.cfdiTipoRelacion || cfdi.cfdisRelacionados) && (
|
||||
<div className="bg-amber-50 rounded-lg p-3 mb-5 border border-amber-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-xs font-semibold text-amber-800 uppercase tracking-wide">CFDI Relacionado</p>
|
||||
{cfdi.cfdiTipoRelacion && (
|
||||
<p className="text-sm text-gray-700 mt-1">
|
||||
<span className="font-medium">Tipo de relación:</span>{' '}
|
||||
{cfdi.cfdiTipoRelacion} — {tipoRelacionLabels[cfdi.cfdiTipoRelacion] || 'Desconocido'}
|
||||
</p>
|
||||
)}
|
||||
{cfdi.cfdisRelacionados && (
|
||||
<div className="text-sm text-gray-700 mt-1">
|
||||
<span className="font-medium">UUIDs relacionados:</span>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{cfdi.cfdisRelacionados.split('|').map((uuid) => uuid.trim()).filter(Boolean).map((uuid, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-block px-2 py-0.5 bg-white border border-amber-200 rounded text-xs font-mono text-gray-600"
|
||||
>
|
||||
{uuid}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Datos del Comprobante */}
|
||||
<div className="grid grid-cols-4 gap-3 mb-5">
|
||||
<div className="bg-gray-50 rounded-lg p-3 text-center">
|
||||
@@ -203,6 +284,7 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="text-left py-3 px-4 font-semibold text-gray-700">Descripción</th>
|
||||
<th className="text-center py-3 px-3 font-semibold text-gray-700 w-24">No. Id.</th>
|
||||
<th className="text-center py-3 px-3 font-semibold text-gray-700 w-20">Cant.</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-700 w-32">P. Unitario</th>
|
||||
<th className="text-right py-3 px-4 font-semibold text-gray-700 w-32">Importe</th>
|
||||
@@ -223,6 +305,9 @@ export const CfdiInvoice = forwardRef<HTMLDivElement, CfdiInvoiceProps>(
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center py-3 px-3 text-gray-700">
|
||||
{concepto.noIdentificacion || <span className="text-gray-300">—</span>}
|
||||
</td>
|
||||
<td className="text-center py-3 px-3 text-gray-700">{concepto.cantidad}</td>
|
||||
<td className="text-right py-3 px-4 text-gray-700">
|
||||
{formatCurrency(concepto.valorUnitario)}
|
||||
|
||||
@@ -14,6 +14,7 @@ interface CfdiConcepto {
|
||||
importe: number;
|
||||
claveProdServ?: string;
|
||||
claveUnidad?: string;
|
||||
noIdentificacion?: string;
|
||||
}
|
||||
|
||||
interface CfdiViewerModalProps {
|
||||
@@ -39,6 +40,7 @@ function parseConceptosFromXml(xmlString: string): CfdiConcepto[] {
|
||||
importe: parseFloat(el.getAttribute('Importe') || '0'),
|
||||
claveProdServ: el.getAttribute('ClaveProdServ') || undefined,
|
||||
claveUnidad: el.getAttribute('ClaveUnidad') || undefined,
|
||||
noIdentificacion: el.getAttribute('NoIdentificacion') || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -75,6 +77,7 @@ export function CfdiViewerModal({ cfdi, open, onClose }: CfdiViewerModalProps) {
|
||||
importe: Number(c.importe),
|
||||
claveProdServ: c.claveProdServ || undefined,
|
||||
claveUnidad: c.claveUnidad || undefined,
|
||||
noIdentificacion: c.noIdentificacion || undefined,
|
||||
})));
|
||||
} else if (cfdi.xmlOriginal) {
|
||||
setConceptos(parseConceptosFromXml(cfdi.xmlOriginal));
|
||||
|
||||
Reference in New Issue
Block a user