refactor(cfdi): descarga masiva de XMLs por filtros en lugar de checkboxes
- Backend: POST /cfdi/download-xmls acepta CfdiFilters, usa getXmlsByFilters con LIMIT 1000 - Frontend: eliminados checkboxes y estado selectedIds; botón Descargar XMLs usa filtros activos - Si >1000 resultados, muestra confirm() de advertencia pero permite proceder - Agregada documentación técnica y changelog
This commit is contained in:
@@ -82,28 +82,33 @@ export async function downloadXmlsZip(req: Request, res: Response, next: NextFun
|
||||
return next(new AppError(400, 'Tenant no configurado'));
|
||||
}
|
||||
|
||||
const ids = req.body.ids as number[];
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return next(new AppError(400, 'Se requiere un array de IDs'));
|
||||
}
|
||||
if (ids.length > 1000) {
|
||||
return next(new AppError(400, 'Máximo 1,000 CFDIs por descarga'));
|
||||
}
|
||||
const filters: CfdiFilters = {
|
||||
tipo: req.body.tipo as any,
|
||||
tipoComprobante: req.body.tipoComprobante as any,
|
||||
estado: req.body.estado as any,
|
||||
fechaInicio: req.body.fechaInicio as string,
|
||||
fechaFin: req.body.fechaFin as string,
|
||||
rfc: req.body.rfc as string,
|
||||
emisor: req.body.emisor as string,
|
||||
receptor: req.body.receptor as string,
|
||||
search: req.body.search as string,
|
||||
contribuyenteId: req.body.contribuyenteId as string,
|
||||
};
|
||||
|
||||
const cfdis = await cfdiService.getXmlsByIds(req.tenantPool, ids);
|
||||
const cfdis = await cfdiService.getCfdiXmlsForZip(req.tenantPool, filters);
|
||||
const zip = new AdmZip();
|
||||
let added = 0;
|
||||
|
||||
for (const cfdi of cfdis) {
|
||||
if (cfdi.xml) {
|
||||
const filename = `${cfdi.uuid || cfdi.id}.xml`;
|
||||
const filename = `${cfdi.uuid || 'cfdi'}.xml`;
|
||||
zip.addFile(filename, Buffer.from(cfdi.xml, 'utf8'));
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
if (added === 0) {
|
||||
return next(new AppError(404, 'No se encontraron XMLs para los CFDIs seleccionados'));
|
||||
return next(new AppError(404, 'No se encontraron XMLs para los filtros aplicados'));
|
||||
}
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
|
||||
@@ -364,6 +364,74 @@ export async function getXmlsByIds(pool: Pool, ids: number[]): Promise<{ id: num
|
||||
return rows.map((r: any) => ({ id: r.id, uuid: r.uuid, xml: r.xml_original || null }));
|
||||
}
|
||||
|
||||
export async function getCfdiXmlsForZip(
|
||||
pool: Pool,
|
||||
filters: CfdiFilters
|
||||
): Promise<{ uuid: string; xml: string | null }[]> {
|
||||
let whereClause = 'WHERE xml_original IS NOT NULL';
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (filters.tipo && !filters.contribuyenteId) {
|
||||
whereClause += ` AND type = $${paramIndex++}`;
|
||||
params.push(filters.tipo);
|
||||
}
|
||||
if (filters.tipoComprobante) {
|
||||
whereClause += ` AND tipo_comprobante = $${paramIndex++}`;
|
||||
params.push(filters.tipoComprobante);
|
||||
}
|
||||
if (filters.estado) {
|
||||
whereClause += ` AND status = $${paramIndex++}`;
|
||||
params.push(filters.estado);
|
||||
}
|
||||
if (filters.fechaInicio) {
|
||||
whereClause += ` AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') >= $${paramIndex++}::date`;
|
||||
params.push(filters.fechaInicio);
|
||||
}
|
||||
if (filters.fechaFin) {
|
||||
whereClause += ` AND COALESCE(fecha_efectiva, fecha_emision - interval '1 hour') <= ($${paramIndex++}::date + interval '1 day')`;
|
||||
params.push(filters.fechaFin);
|
||||
}
|
||||
if (filters.rfc) {
|
||||
whereClause += ` AND (rfc_emisor ILIKE $${paramIndex} OR rfc_receptor ILIKE $${paramIndex++})`;
|
||||
params.push(`%${filters.rfc}%`);
|
||||
}
|
||||
if (filters.emisor) {
|
||||
whereClause += ` AND (rfc_emisor ILIKE $${paramIndex} OR nombre_emisor ILIKE $${paramIndex++})`;
|
||||
params.push(`%${filters.emisor}%`);
|
||||
}
|
||||
if (filters.receptor) {
|
||||
whereClause += ` AND (rfc_receptor ILIKE $${paramIndex} OR nombre_receptor ILIKE $${paramIndex++})`;
|
||||
params.push(`%${filters.receptor}%`);
|
||||
}
|
||||
if (filters.search) {
|
||||
whereClause += ` AND (uuid ILIKE $${paramIndex} OR nombre_emisor ILIKE $${paramIndex} OR nombre_receptor ILIKE $${paramIndex} OR rfc_emisor ILIKE $${paramIndex} OR rfc_receptor ILIKE $${paramIndex++})`;
|
||||
params.push(`%${filters.search}%`);
|
||||
}
|
||||
if (filters.contribuyenteId) {
|
||||
if (filters.tipo === 'EMITIDO') {
|
||||
whereClause += ` AND rfc_emisor = (SELECT rfc FROM contribuyentes WHERE entidad_id = $${paramIndex++})`;
|
||||
params.push(filters.contribuyenteId);
|
||||
} else if (filters.tipo === 'RECIBIDO') {
|
||||
whereClause += ` AND rfc_receptor = (SELECT rfc FROM contribuyentes WHERE entidad_id = $${paramIndex++})`;
|
||||
params.push(filters.contribuyenteId);
|
||||
} else {
|
||||
whereClause += ` AND (contribuyente_id = $${paramIndex} OR rfc_emisor = (SELECT rfc FROM contribuyentes WHERE entidad_id = $${paramIndex}) OR rfc_receptor = (SELECT rfc FROM contribuyentes WHERE entidad_id = $${paramIndex++}))`;
|
||||
params.push(filters.contribuyenteId);
|
||||
}
|
||||
}
|
||||
|
||||
params.push(1000);
|
||||
const { rows } = await pool.query(`
|
||||
SELECT uuid, xml_original FROM cfdis
|
||||
${whereClause}
|
||||
ORDER BY fecha_emision DESC
|
||||
LIMIT $${paramIndex++}
|
||||
`, params);
|
||||
|
||||
return rows.map((r: any) => ({ uuid: r.uuid, xml: r.xml_original || null }));
|
||||
}
|
||||
|
||||
export interface CreateCfdiData {
|
||||
uuid: string;
|
||||
type: 'EMITIDO' | 'RECIBIDO';
|
||||
|
||||
Reference in New Issue
Block a user