- Add Promise<void> return type to async handlers with conditional returns - Change 'return res.status()' to 'res.status(); return;' pattern - Fixed controllers: ap, ar, bank-reconciliation, cash-flow, reports, accounting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
420 lines
12 KiB
TypeScript
420 lines
12 KiB
TypeScript
/**
|
|
* ReportsController - Controlador de Reportes Financieros
|
|
*
|
|
* Endpoints para estados financieros y exportación.
|
|
*
|
|
* @module Finance
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import { DataSource } from 'typeorm';
|
|
import { FinancialReportsService, ERPIntegrationService } from '../services';
|
|
|
|
export function createReportsController(dataSource: DataSource): Router {
|
|
const router = Router();
|
|
const reportsService = new FinancialReportsService(dataSource);
|
|
const integrationService = new ERPIntegrationService(dataSource);
|
|
|
|
// ==================== ESTADOS FINANCIEROS ====================
|
|
|
|
/**
|
|
* GET /balance-sheet
|
|
* Genera balance general
|
|
*/
|
|
router.get('/balance-sheet', async (req: Request, res: Response) => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const asOfDate = req.query.asOfDate
|
|
? new Date(req.query.asOfDate as string)
|
|
: new Date();
|
|
|
|
const options = {
|
|
projectId: req.query.projectId as string,
|
|
};
|
|
|
|
const report = await reportsService.generateBalanceSheet(ctx, asOfDate, options);
|
|
res.json(report);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /income-statement
|
|
* Genera estado de resultados
|
|
*/
|
|
router.get('/income-statement', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
projectId: req.query.projectId as string,
|
|
};
|
|
|
|
const report = await reportsService.generateIncomeStatement(
|
|
ctx,
|
|
periodStart,
|
|
periodEnd,
|
|
options
|
|
);
|
|
res.json(report);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /cash-flow-statement
|
|
* Genera estado de flujo de efectivo
|
|
*/
|
|
router.get('/cash-flow-statement', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
projectId: req.query.projectId as string,
|
|
};
|
|
|
|
const report = await reportsService.generateCashFlowStatement(
|
|
ctx,
|
|
periodStart,
|
|
periodEnd,
|
|
options
|
|
);
|
|
res.json(report);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /trial-balance
|
|
* Genera balanza de comprobación
|
|
*/
|
|
router.get('/trial-balance', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
projectId: req.query.projectId as string,
|
|
includeZeroBalances: req.query.includeZeroBalances === 'true',
|
|
};
|
|
|
|
const report = await reportsService.generateTrialBalance(
|
|
ctx,
|
|
periodStart,
|
|
periodEnd,
|
|
options
|
|
);
|
|
res.json(report);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /account-statement/:accountId
|
|
* Genera estado de cuenta
|
|
*/
|
|
router.get('/account-statement/:accountId', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const report = await reportsService.generateAccountStatement(
|
|
ctx,
|
|
req.params.accountId,
|
|
periodStart,
|
|
periodEnd
|
|
);
|
|
res.json(report);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /summary
|
|
* Obtiene resumen financiero
|
|
*/
|
|
router.get('/summary', async (req: Request, res: Response) => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const summary = await reportsService.getFinancialSummary(ctx);
|
|
res.json(summary);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
// ==================== EXPORTACIÓN ====================
|
|
|
|
/**
|
|
* GET /export/sap
|
|
* Exporta pólizas para SAP
|
|
*/
|
|
router.get('/export/sap', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
companyCode: req.query.companyCode as string,
|
|
documentType: req.query.documentType as string,
|
|
journalNumber: req.query.journalNumber
|
|
? parseInt(req.query.journalNumber as string)
|
|
: undefined,
|
|
};
|
|
|
|
const result = await integrationService.exportToSAP(ctx, periodStart, periodEnd, options);
|
|
|
|
res.setHeader('Content-Type', 'text/plain');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
|
|
res.send(result.data);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /export/contpaqi
|
|
* Exporta pólizas para CONTPAQi
|
|
*/
|
|
router.get('/export/contpaqi', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
polizaTipo: req.query.polizaTipo
|
|
? parseInt(req.query.polizaTipo as string)
|
|
: undefined,
|
|
diario: req.query.diario ? parseInt(req.query.diario as string) : undefined,
|
|
};
|
|
|
|
const result = await integrationService.exportToCONTPAQi(
|
|
ctx,
|
|
periodStart,
|
|
periodEnd,
|
|
options
|
|
);
|
|
|
|
res.setHeader('Content-Type', 'text/plain');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
|
|
res.send(result.data);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /export/cfdi-polizas
|
|
* Exporta pólizas en formato CFDI
|
|
*/
|
|
router.get('/export/cfdi-polizas', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
tipoSolicitud: req.query.tipoSolicitud as string,
|
|
numOrden: req.query.numOrden as string,
|
|
numTramite: req.query.numTramite as string,
|
|
};
|
|
|
|
const result = await integrationService.exportCFDIPolizas(
|
|
ctx,
|
|
periodStart,
|
|
periodEnd,
|
|
options
|
|
);
|
|
|
|
res.setHeader('Content-Type', 'application/xml');
|
|
res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
|
|
res.send(result.data);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /export/chart-of-accounts
|
|
* Exporta catálogo de cuentas
|
|
*/
|
|
router.get('/export/chart-of-accounts', async (req: Request, res: Response) => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const format = (req.query.format as 'csv' | 'xml' | 'json') || 'csv';
|
|
const result = await integrationService.exportChartOfAccounts(ctx, format);
|
|
|
|
const contentType =
|
|
format === 'xml'
|
|
? 'application/xml'
|
|
: format === 'json'
|
|
? 'application/json'
|
|
: 'text/csv';
|
|
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
|
|
res.send(typeof result.data === 'object' ? JSON.stringify(result.data, null, 2) : result.data);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /export/trial-balance
|
|
* Exporta balanza de comprobación
|
|
*/
|
|
router.get('/export/trial-balance', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const periodStart = new Date(req.query.periodStart as string);
|
|
const periodEnd = new Date(req.query.periodEnd as string);
|
|
|
|
if (isNaN(periodStart.getTime()) || isNaN(periodEnd.getTime())) {
|
|
res.status(400).json({ error: 'Fechas inválidas' });
|
|
return;
|
|
}
|
|
|
|
const format = (req.query.format as 'csv' | 'xml' | 'json') || 'csv';
|
|
const result = await integrationService.exportTrialBalance(
|
|
ctx,
|
|
periodStart,
|
|
periodEnd,
|
|
format
|
|
);
|
|
|
|
const contentType =
|
|
format === 'xml'
|
|
? 'application/xml'
|
|
: format === 'json'
|
|
? 'application/json'
|
|
: 'text/csv';
|
|
|
|
res.setHeader('Content-Type', contentType);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${result.filename}"`);
|
|
res.send(typeof result.data === 'object' ? JSON.stringify(result.data, null, 2) : result.data);
|
|
} catch (error) {
|
|
res.status(500).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
// ==================== IMPORTACIÓN ====================
|
|
|
|
/**
|
|
* POST /import/chart-of-accounts
|
|
* Importa catálogo de cuentas
|
|
*/
|
|
router.post('/import/chart-of-accounts', async (req: Request, res: Response): Promise<void> => {
|
|
try {
|
|
const ctx = {
|
|
tenantId: req.headers['x-tenant-id'] as string,
|
|
userId: (req as any).user?.id,
|
|
};
|
|
|
|
const { data, format } = req.body;
|
|
if (!data || !format) {
|
|
res.status(400).json({ error: 'Se requieren datos y formato' });
|
|
return;
|
|
}
|
|
|
|
const result = await integrationService.importChartOfAccounts(ctx, data, format);
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(400).json({ error: (error as Error).message });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|