feat(facturacion): seccion CFDIs Relacionados para tipos I y E

This commit is contained in:
Horux Dev
2026-04-28 15:22:24 +00:00
parent 066c9cdb74
commit ee9c76612e
5 changed files with 160 additions and 45 deletions

View File

@@ -15,7 +15,7 @@ import { searchClaveProdServ } from '@/lib/api/catalogos';
import { apiClient } from '@/lib/api/client';
import type { InvoiceData, InvoiceLineItem, RfcSearchResult, CfdiPpdPendiente, ConceptoPrevio } from '@/lib/api/facturacion';
import { searchRfcs, getCfdisPpd, searchConceptos } from '@/lib/api/facturacion';
import { Plus, Trash2, Send, Receipt, Search, Check, X, FileSearch, AlertTriangle } from 'lucide-react';
import { Plus, Trash2, Send, Receipt, Search, Check, X, FileSearch, AlertTriangle, Link2 } from 'lucide-react';
import { cn } from '@horux/shared-ui';
interface TaxLine {
@@ -25,6 +25,29 @@ interface TaxLine {
factor: string; // Tasa, Cuota, Exento
}
interface RelatedDocForm {
relationship: string;
uuids: string[];
}
const RELACION_OPTIONS: Record<string, { value: string; label: string }[]> = {
I: [
{ value: '04', label: '04 - Sustitución de CFDI previos' },
{ value: '05', label: '05 - Traslados de mercancias facturados previamente' },
{ value: '06', label: '06 - Factura generada por traslados previos' },
{ value: '07', label: '07 - CFDI por aplicación de anticipo' },
{ value: '08', label: '08 - Factura generada por pagos en parcialidades' },
{ value: '09', label: '09 - Factura generada por pagos diferidos' },
],
E: [
{ value: '01', label: '01 - Nota de crédito de documentos relacionados' },
{ value: '02', label: '02 - Nota de débito de documentos relacionados' },
{ value: '03', label: '03 - Devolución de mercancía sobre facturas o traslados previos' },
{ value: '04', label: '04 - Sustitución de los CFDI previos' },
{ value: '07', label: '07 - CFDI por aplicación de anticipo' },
],
};
interface ConceptoForm {
description: string;
productKey: string;
@@ -188,7 +211,7 @@ const TIPO_CONFIG: Record<string, {
defaultFormaPago: string;
defaultMetodoPago: string;
}> = {
I: { label: 'Ingreso', needsConceptos: true, needsPaymentComplement: false, needsRelated: false, defaultUso: 'G03', defaultFormaPago: '99', defaultMetodoPago: 'PUE' },
I: { label: 'Ingreso', needsConceptos: true, needsPaymentComplement: false, needsRelated: true, defaultUso: 'G03', defaultFormaPago: '99', defaultMetodoPago: 'PUE' },
E: { label: 'Egreso (Nota de Crédito)', needsConceptos: true, needsPaymentComplement: false, needsRelated: true, defaultUso: 'G02', defaultFormaPago: '99', defaultMetodoPago: 'PUE' },
P: { label: 'Pago', needsConceptos: false, needsPaymentComplement: true, needsRelated: false, defaultUso: 'CP01', defaultFormaPago: '99', defaultMetodoPago: 'PPD' },
T: { label: 'Traslado', needsConceptos: true, needsPaymentComplement: false, needsRelated: false, defaultUso: 'S01', defaultFormaPago: '99', defaultMetodoPago: 'PUE' },
@@ -306,9 +329,8 @@ export default function FacturacionPage() {
// Conceptos
const [conceptos, setConceptos] = useState<ConceptoForm[]>([{ ...emptyConcepto }]);
// Documento relacionado (Egreso)
const [relatedUuid, setRelatedUuid] = useState('');
const [relatedRelationship, setRelatedRelationship] = useState('01');
// CFDIs relacionados (Ingreso / Egreso)
const [relatedDocs, setRelatedDocs] = useState<RelatedDocForm[]>([]);
// Complemento de pago
const [pagoFecha, setPagoFecha] = useState(new Date().toISOString().slice(0, 10));
@@ -344,8 +366,7 @@ export default function FacturacionPage() {
setFolio('');
setCondiciones('');
setConceptos([{ ...emptyConcepto, unitKey: defaultUnit }]);
setRelatedUuid('');
setRelatedRelationship('01');
setRelatedDocs([]);
setPagoUuid('');
setPagoMonto(0);
setPagoParcialidad(1);
@@ -635,8 +656,10 @@ export default function FacturacionPage() {
}));
}
if (config.needsRelated && relatedUuid) {
data.relatedDocuments = [{ uuid: relatedUuid, relationship: relatedRelationship }];
if (config.needsRelated && relatedDocs.length > 0) {
data.relatedDocuments = relatedDocs
.filter(r => r.uuids.length > 0)
.map(r => ({ relationship: r.relationship, uuids: r.uuids.filter(u => u.trim() !== '') }));
}
if (config.needsPaymentComplement) {
@@ -1118,36 +1141,6 @@ export default function FacturacionPage() {
</CardContent>
</Card>
{/* Documento Relacionado (Egreso) */}
{config.needsRelated && (
<Card>
<CardHeader>
<CardTitle className="text-base">Documento Relacionado</CardTitle>
<CardDescription>UUID del CFDI de ingreso al que aplica esta nota de crédito</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>UUID del CFDI relacionado</Label>
<Input value={relatedUuid} onChange={e => setRelatedUuid(e.target.value.toUpperCase())} placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" required />
</div>
<div className="space-y-2">
<Label>Tipo de Relación</Label>
<Select value={relatedRelationship} onValueChange={setRelatedRelationship}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="01">01 - Nota de crédito</SelectItem>
<SelectItem value="02">02 - Nota de débito</SelectItem>
<SelectItem value="03">03 - Devolución de mercancía</SelectItem>
<SelectItem value="04">04 - Sustitución de CFDI previo</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
)}
{/* Complemento de Pago */}
{config.needsPaymentComplement && (
<Card>
@@ -1438,6 +1431,116 @@ export default function FacturacionPage() {
</Card>
)}
{/* CFDIs Relacionados */}
{config.needsRelated && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">CFDIs Relacionados</CardTitle>
<CardDescription>Relaciona esta factura con otros comprobantes previos</CardDescription>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setRelatedDocs([...relatedDocs, { relationship: tipoComprobante === 'E' ? '01' : '04', uuids: [''] }])}
>
<Plus className="h-4 w-4 mr-1" /> Relación
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{relatedDocs.length === 0 && (
<p className="text-sm text-muted-foreground">Sin relaciones. Agrega una si esta factura está vinculada a otros CFDI.</p>
)}
{relatedDocs.map((rel, rIdx) => (
<div key={rIdx} className="p-4 border rounded-lg space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Link2 className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium text-muted-foreground">Relación {rIdx + 1}</span>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive"
onClick={() => setRelatedDocs(relatedDocs.filter((_, i) => i !== rIdx))}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Tipo de Relación</Label>
<Select
value={rel.relationship}
onValueChange={v => {
const updated = [...relatedDocs];
updated[rIdx].relationship = v;
setRelatedDocs(updated);
}}
>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{RELACION_OPTIONS[tipoComprobante]?.map(opt => (
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>UUIDs relacionados</Label>
{rel.uuids.map((uuid, uIdx) => (
<div key={uIdx} className="flex gap-2">
<Input
value={uuid}
onChange={e => {
const updated = [...relatedDocs];
updated[rIdx].uuids[uIdx] = e.target.value.toUpperCase();
setRelatedDocs(updated);
}}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
className="font-mono text-sm"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="h-10 w-10 text-destructive flex-shrink-0"
onClick={() => {
const updated = [...relatedDocs];
updated[rIdx].uuids = updated[rIdx].uuids.filter((_, i) => i !== uIdx);
setRelatedDocs(updated);
}}
disabled={rel.uuids.length === 1}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
<Button
type="button"
variant="ghost"
size="sm"
className="text-xs"
onClick={() => {
const updated = [...relatedDocs];
updated[rIdx].uuids.push('');
setRelatedDocs(updated);
}}
>
<Plus className="h-3 w-3 mr-1" /> Agregar UUID
</Button>
</div>
</div>
))}
</CardContent>
</Card>
)}
{/* Resumen y Emitir */}
<Card>
<CardContent className="pt-6">

View File

@@ -69,6 +69,7 @@ export interface InvoiceData {
series?: string;
folioNumber?: number;
conditions?: string;
relatedDocuments?: Array<{ relationship: string; uuids: string[] }>;
}
export interface InvoiceResult {