fix(impuestos): desactivar JIT en queries con subplans correlacionados
- Agrega helper withJitOff en impuestos.service.ts - Ejecuta getResumenIva, getIvaMensual y readResumenIvaFromCache con SET LOCAL jit = off - Evita compilación JIT de ~17s en queries con costo estimado alto feat(contribuyentes): auto-asignar a cartera del supervisor - Al crear contribuyente con supervisorUserId, se agrega automáticamente a todas las carteras top-level del supervisor feat(permisos): restricciones de UI por rol en contribuyentes - Oculta botón Add-ons para roles distintos de owner/cfo - Oculta botón Eliminar contribuyente para no-owner - Oculta botón Agregar RFC para auxiliar/visor/cliente/contador feat(cfdi): ver CFDI desde conceptos y forma de pago en Excel - Agrega botón Ver CFDI en cada fila de la tabla de Conceptos - Agrega columna Forma de Pago en export Excel de CFDIs - Agrega columna Forma de Pago en export individual de CFDI chore(migraciones): índices GIN para relaciones de activos - 048: índices btree parciales para activos - 049: índices GIN para cfdis_relacionados y uuid_relacionado
This commit is contained in:
@@ -71,9 +71,9 @@ class TenantConnectionManager {
|
|||||||
user: connectionOverride?.user ?? this.dbConfig.user,
|
user: connectionOverride?.user ?? this.dbConfig.user,
|
||||||
password: connectionOverride?.password ?? this.dbConfig.password,
|
password: connectionOverride?.password ?? this.dbConfig.password,
|
||||||
database: databaseName,
|
database: databaseName,
|
||||||
max: 3,
|
max: 10,
|
||||||
idleTimeoutMillis: 300_000,
|
idleTimeoutMillis: 300_000,
|
||||||
connectionTimeoutMillis: 10_000,
|
connectionTimeoutMillis: 30_000,
|
||||||
};
|
};
|
||||||
|
|
||||||
pool = new Pool(poolConfig);
|
pool = new Pool(poolConfig);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { AppError } from '../middlewares/error.middleware.js';
|
|||||||
/**
|
/**
|
||||||
* Valida que el auxiliar pertenezca al supervisor (o que el caller sea owner).
|
* Valida que el auxiliar pertenezca al supervisor (o que el caller sea owner).
|
||||||
* Owner puede asignar a cualquier auxiliar del tenant.
|
* Owner puede asignar a cualquier auxiliar del tenant.
|
||||||
|
* La relación se infiere desde carteras (directas y subcarteras) con fallback
|
||||||
|
* a la tabla legacy auxiliar_supervisores.
|
||||||
*/
|
*/
|
||||||
async function validarAuxiliarDelSupervisor(
|
async function validarAuxiliarDelSupervisor(
|
||||||
pool: import('pg').Pool,
|
pool: import('pg').Pool,
|
||||||
@@ -17,10 +19,22 @@ async function validarAuxiliarDelSupervisor(
|
|||||||
if (callerRole === 'owner') return;
|
if (callerRole === 'owner') return;
|
||||||
|
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT 1 FROM auxiliar_supervisores
|
`SELECT 1 FROM (
|
||||||
WHERE auxiliar_user_id = $1 AND supervisor_user_id = $2
|
SELECT c.auxiliar_user_id
|
||||||
LIMIT 1`,
|
FROM carteras c
|
||||||
[auxiliarUserId, supervisorUserId],
|
WHERE c.supervisor_user_id = $1
|
||||||
|
AND c.auxiliar_user_id = $2
|
||||||
|
UNION
|
||||||
|
SELECT sub.auxiliar_user_id
|
||||||
|
FROM carteras sub
|
||||||
|
JOIN carteras p ON p.id = sub.parent_id
|
||||||
|
WHERE p.supervisor_user_id = $1
|
||||||
|
AND sub.auxiliar_user_id = $2
|
||||||
|
UNION
|
||||||
|
SELECT auxiliar_user_id FROM auxiliar_supervisores
|
||||||
|
WHERE supervisor_user_id = $1 AND auxiliar_user_id = $2
|
||||||
|
) t LIMIT 1`,
|
||||||
|
[supervisorUserId, auxiliarUserId],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
@@ -28,10 +42,30 @@ async function validarAuxiliarDelSupervisor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida que el auxiliar tenga al contribuyente en alguna de sus subcarteras.
|
||||||
|
* Si no hay ningún auxiliar con ese contribuyente en su subcartera, la asignación
|
||||||
|
* se rechaza (el supervisor debe agregar el contribuyente a una subcartera primero).
|
||||||
|
*/
|
||||||
|
async function validarAuxiliarEnSubcartera(
|
||||||
|
pool: import('pg').Pool,
|
||||||
|
contribuyenteId: string,
|
||||||
|
auxiliarUserId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const elegibles = await asignacionesService.getAuxiliaresElegibles(pool, contribuyenteId);
|
||||||
|
if (elegibles.length === 0) {
|
||||||
|
throw new AppError(403, 'Ningún auxiliar tiene este contribuyente en su subcartera');
|
||||||
|
}
|
||||||
|
if (!elegibles.includes(auxiliarUserId)) {
|
||||||
|
throw new AppError(403, 'El auxiliar no tiene este contribuyente en ninguna de sus subcarteras');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Obligaciones ──
|
// ── Obligaciones ──
|
||||||
|
|
||||||
export async function asignarObligacion(req: Request, res: Response, next: NextFunction) {
|
export async function asignarObligacion(req: Request, res: Response, next: NextFunction) {
|
||||||
try {
|
try {
|
||||||
|
const contribuyenteId = String(req.params.id);
|
||||||
const obligacionId = String(req.params.obligacionId);
|
const obligacionId = String(req.params.obligacionId);
|
||||||
const schema = z.object({ auxiliarUserId: z.string().uuid() });
|
const schema = z.object({ auxiliarUserId: z.string().uuid() });
|
||||||
const { auxiliarUserId } = schema.parse(req.body);
|
const { auxiliarUserId } = schema.parse(req.body);
|
||||||
@@ -42,6 +76,11 @@ export async function asignarObligacion(req: Request, res: Response, next: NextF
|
|||||||
auxiliarUserId,
|
auxiliarUserId,
|
||||||
req.user!.role,
|
req.user!.role,
|
||||||
);
|
);
|
||||||
|
await validarAuxiliarEnSubcartera(
|
||||||
|
req.tenantPool!,
|
||||||
|
contribuyenteId,
|
||||||
|
auxiliarUserId,
|
||||||
|
);
|
||||||
|
|
||||||
await asignacionesService.asignarObligacion(
|
await asignacionesService.asignarObligacion(
|
||||||
req.tenantPool!,
|
req.tenantPool!,
|
||||||
@@ -80,6 +119,19 @@ export async function asignarTarea(req: Request, res: Response, next: NextFuncti
|
|||||||
req.user!.role,
|
req.user!.role,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Obtener contribuyenteId de la tarea para validar subcartera
|
||||||
|
const { rows } = await req.tenantPool!.query<{ contribuyente_id: string }>(
|
||||||
|
`SELECT contribuyente_id FROM tareas_catalogo WHERE id = $1 LIMIT 1`,
|
||||||
|
[tareaId],
|
||||||
|
);
|
||||||
|
if (rows.length > 0) {
|
||||||
|
await validarAuxiliarEnSubcartera(
|
||||||
|
req.tenantPool!,
|
||||||
|
rows[0].contribuyente_id,
|
||||||
|
auxiliarUserId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await asignacionesService.asignarTarea(
|
await asignacionesService.asignarTarea(
|
||||||
req.tenantPool!,
|
req.tenantPool!,
|
||||||
tareaId,
|
tareaId,
|
||||||
@@ -135,3 +187,11 @@ export async function listSinAsignar(req: Request, res: Response, next: NextFunc
|
|||||||
res.json({ obligaciones, tareas });
|
res.json({ obligaciones, tareas });
|
||||||
} catch (err) { next(err); }
|
} catch (err) { next(err); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listAuxiliaresElegibles(req: Request, res: Response, next: NextFunction) {
|
||||||
|
try {
|
||||||
|
const contribuyenteId = String(req.params.contribuyenteId);
|
||||||
|
const auxIds = await asignacionesService.getAuxiliaresElegibles(req.tenantPool!, contribuyenteId);
|
||||||
|
res.json({ auxiliares: auxIds });
|
||||||
|
} catch (err) { next(err); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Request, Response, NextFunction } from 'express';
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import * as contribuyenteService from '../services/contribuyente.service.js';
|
import * as contribuyenteService from '../services/contribuyente.service.js';
|
||||||
|
import * as carteraService from '../services/cartera.service.js';
|
||||||
import { AppError } from '../middlewares/error.middleware.js';
|
import { AppError } from '../middlewares/error.middleware.js';
|
||||||
import { getEntidadesVisibles } from '../utils/entidades-visibles.js';
|
import { getEntidadesVisibles } from '../utils/entidades-visibles.js';
|
||||||
import { adjustDespachoOverage } from '../services/payment/addon.service.js';
|
import { adjustDespachoOverage } from '../services/payment/addon.service.js';
|
||||||
@@ -77,6 +78,19 @@ export async function create(req: Request, res: Response, next: NextFunction) {
|
|||||||
|
|
||||||
const row = await contribuyenteService.createContribuyente(req.tenantPool!, data);
|
const row = await contribuyenteService.createContribuyente(req.tenantPool!, data);
|
||||||
|
|
||||||
|
// Si se asignó un supervisor, agregar el contribuyente a todas las carteras
|
||||||
|
// top-level de ese supervisor para que aparezca directamente en su vista.
|
||||||
|
if (data.supervisorUserId) {
|
||||||
|
try {
|
||||||
|
const carteras = await carteraService.listCarteras(req.tenantPool!, data.supervisorUserId);
|
||||||
|
await Promise.all(
|
||||||
|
carteras.map(c => carteraService.addEntidadToCartera(req.tenantPool!, c.id, row.id))
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[Contribuyente] Auto-assign to cartera failed (non-blocking):', err.message || err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ajuste de overage despacho: si el tenant pasa de 100 a 101+ RFCs, crea
|
// Ajuste de overage despacho: si el tenant pasa de 100 a 101+ RFCs, crea
|
||||||
// el addon y devuelve paymentUrl para que el frontend redirija al usuario.
|
// el addon y devuelve paymentUrl para que el frontend redirija al usuario.
|
||||||
// Fail-soft: si falla el addon, el contribuyente queda creado y se loguea.
|
// Fail-soft: si falla el addon, el contribuyente queda creado y se loguea.
|
||||||
|
|||||||
11
apps/api/src/migrations/tenant/048_cfdis_activos_indices.sql
Normal file
11
apps/api/src/migrations/tenant/048_cfdis_activos_indices.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Índices para acelerar los filtros de "Considerar activos" en Impuestos.
|
||||||
|
|
||||||
|
-- Lookup rápido de facturas tipo I con uso de activo fijo
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cfdis_tipo_uso_activos
|
||||||
|
ON cfdis(tipo_comprobante, uso_cfdi)
|
||||||
|
WHERE tipo_comprobante = 'I' AND uso_cfdi IN ('I01','I02','I03','I04','I05','I06','I07','I08');
|
||||||
|
|
||||||
|
-- Filtrar E's que tienen relacionados (reduce el universo del anti-join)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cfdis_tipo_relacionados
|
||||||
|
ON cfdis(tipo_comprobante)
|
||||||
|
WHERE cfdis_relacionados IS NOT NULL;
|
||||||
11
apps/api/src/migrations/tenant/049_cfdis_relaciones_gin.sql
Normal file
11
apps/api/src/migrations/tenant/049_cfdis_relaciones_gin.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- Índices GIN para acelerar búsquedas de activos en cfdis_relacionados y uuid_relacionado.
|
||||||
|
-- El filtro "Considerar activos" usa string_to_array(..., '|') para buscar UUIDs
|
||||||
|
-- relacionados; el índice GIN permite búsquedas @> y ANY eficientes sobre arrays.
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cfdis_relacionados_gin
|
||||||
|
ON cfdis USING gin(string_to_array(LOWER(cfdis_relacionados), '|'))
|
||||||
|
WHERE cfdis_relacionados IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_cfdis_uuid_relacionado_gin
|
||||||
|
ON cfdis USING gin(string_to_array(LOWER(uuid_relacionado), '|'))
|
||||||
|
WHERE uuid_relacionado IS NOT NULL;
|
||||||
@@ -16,6 +16,7 @@ router.get('/supervisores', authorize('owner'), ctrl.getSupervisores);
|
|||||||
router.get('/asignaciones', authorize('owner', 'supervisor'), asignacionesCtrl.listPorSupervisor);
|
router.get('/asignaciones', authorize('owner', 'supervisor'), asignacionesCtrl.listPorSupervisor);
|
||||||
router.get('/asignaciones/mias', authorize('auxiliar'), asignacionesCtrl.listPorAuxiliar);
|
router.get('/asignaciones/mias', authorize('auxiliar'), asignacionesCtrl.listPorAuxiliar);
|
||||||
router.get('/asignaciones/sin-asignar', authorize('owner', 'supervisor'), asignacionesCtrl.listSinAsignar);
|
router.get('/asignaciones/sin-asignar', authorize('owner', 'supervisor'), asignacionesCtrl.listSinAsignar);
|
||||||
|
router.get('/asignaciones/auxiliares-elegibles/:contribuyenteId', authorize('owner', 'supervisor'), asignacionesCtrl.listAuxiliaresElegibles);
|
||||||
|
|
||||||
// Read: owner + supervisor + auxiliar
|
// Read: owner + supervisor + auxiliar
|
||||||
router.get('/', authorize('owner', 'supervisor', 'auxiliar'), ctrl.list);
|
router.get('/', authorize('owner', 'supervisor', 'auxiliar'), ctrl.list);
|
||||||
|
|||||||
@@ -24,44 +24,62 @@
|
|||||||
* el de activos aplica también pero algunos predicados son no-op funcional
|
* el de activos aplica también pero algunos predicados son no-op funcional
|
||||||
* en subqueries que filtran por tipo_comprobante específico (Postgres los
|
* en subqueries que filtran por tipo_comprobante específico (Postgres los
|
||||||
* optimiza away).
|
* optimiza away).
|
||||||
|
*
|
||||||
|
* OPTIMIZACIÓN: los subqueries de exclusiones de activos se reescribieron
|
||||||
|
* para usar subqueries NO-correlacionados donde sea posible (casos 1-3).
|
||||||
|
* Esto permite a PostgreSQL ejecutar el subquery una sola vez por query
|
||||||
|
* principal, en lugar de una vez por cada fila. Solo el caso 4 (anticipo
|
||||||
|
* referenciado por I07) requiere un correlated EXISTS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ACTIVOS_USOS = "('I01','I02','I03','I04','I05','I06','I07','I08')";
|
const ACTIVOS_USOS = "('I01','I02','I03','I04','I05','I06','I07','I08')";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subquery no-correlacionado que devuelve todos los UUIDs de facturas tipo I
|
||||||
|
* con uso de activo. Usado para lookups P→I y E→I.
|
||||||
|
*/
|
||||||
|
const UUIDS_ACTIVOS = `SELECT LOWER(uuid) AS uuid FROM cfdis WHERE tipo_comprobante = 'I' AND uso_cfdi IN ${ACTIVOS_USOS}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subquery no-correlacionado que devuelve todos los UUIDs de E's que
|
||||||
|
* referencian un activo (directamente I-activo, o indirectamente P→I-activo).
|
||||||
|
*
|
||||||
|
* Usa JOIN + UNION en lugar de EXISTS + OR para que PostgreSQL pueda usar
|
||||||
|
* índices de forma más efectiva (especialmente el GIN en cfdis_relacionados).
|
||||||
|
*/
|
||||||
|
const UUIDS_E_DE_ACTIVOS = `
|
||||||
|
SELECT e.uuid
|
||||||
|
FROM cfdis e
|
||||||
|
JOIN cfdis r_act ON LOWER(r_act.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
|
||||||
|
WHERE e.tipo_comprobante = 'E'
|
||||||
|
AND e.cfdis_relacionados IS NOT NULL
|
||||||
|
AND r_act.tipo_comprobante = 'I'
|
||||||
|
AND r_act.uso_cfdi IN ${ACTIVOS_USOS}
|
||||||
|
UNION ALL
|
||||||
|
SELECT e.uuid
|
||||||
|
FROM cfdis e
|
||||||
|
JOIN cfdis r_act ON LOWER(r_act.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
|
||||||
|
JOIN cfdis pi_act ON LOWER(pi_act.uuid) = LOWER(r_act.uuid_relacionado)
|
||||||
|
WHERE e.tipo_comprobante = 'E'
|
||||||
|
AND e.cfdis_relacionados IS NOT NULL
|
||||||
|
AND r_act.tipo_comprobante = 'P'
|
||||||
|
AND pi_act.tipo_comprobante = 'I'
|
||||||
|
AND pi_act.uso_cfdi IN ${ACTIVOS_USOS}
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Predicado SQL que detecta si el row actual (sin alias de tabla, asume
|
* Predicado SQL que detecta si el row actual (sin alias de tabla, asume
|
||||||
* `FROM cfdis`) referencia un activo directamente (I), indirectamente vía
|
* `FROM cfdis`) referencia un activo directamente (I), indirectamente vía
|
||||||
* pago (P→I), o transitivamente vía relación (E→I, E→P→I).
|
* pago (P→I), o transitivamente vía relación (E→I, E→P→I).
|
||||||
*
|
|
||||||
* IMPORTANTE — qualifying outer refs: dentro de los subqueries `cfdis i_act`
|
|
||||||
* y `cfdis r_act`, la tabla interna también tiene columnas `uuid_relacionado`
|
|
||||||
* y `cfdis_relacionados`. Una referencia no-qualificada las resolvería a las
|
|
||||||
* columnas internas (NO al row outer), volviendo el predicado a no-op.
|
|
||||||
* Por eso usamos `cfdis.uuid_relacionado` y `cfdis.cfdis_relacionados`
|
|
||||||
* explícitamente — fuerza la resolución al outer.
|
|
||||||
*/
|
*/
|
||||||
function activosExclusionNoAlias(): string {
|
function activosExclusionNoAlias(): string {
|
||||||
return `
|
return `
|
||||||
AND NOT (tipo_comprobante = 'I' AND uso_cfdi IN ${ACTIVOS_USOS})
|
AND NOT (tipo_comprobante = 'I' AND uso_cfdi IN ${ACTIVOS_USOS})
|
||||||
AND NOT (tipo_comprobante = 'P' AND EXISTS (
|
AND NOT (tipo_comprobante = 'P' AND EXISTS (
|
||||||
SELECT 1 FROM cfdis i_act
|
SELECT 1 FROM (${UUIDS_ACTIVOS}) ua
|
||||||
WHERE LOWER(i_act.uuid) = LOWER(cfdis.uuid_relacionado)
|
WHERE ua.uuid = ANY(string_to_array(LOWER(uuid_relacionado), '|'))
|
||||||
AND i_act.tipo_comprobante = 'I'
|
|
||||||
AND i_act.uso_cfdi IN ${ACTIVOS_USOS}
|
|
||||||
))
|
|
||||||
AND NOT (tipo_comprobante = 'E' AND cfdis.cfdis_relacionados IS NOT NULL AND EXISTS (
|
|
||||||
SELECT 1 FROM cfdis r_act
|
|
||||||
WHERE LOWER(r_act.uuid) = ANY(string_to_array(LOWER(cfdis.cfdis_relacionados), '|'))
|
|
||||||
AND (
|
|
||||||
(r_act.tipo_comprobante = 'I' AND r_act.uso_cfdi IN ${ACTIVOS_USOS})
|
|
||||||
OR (r_act.tipo_comprobante = 'P' AND EXISTS (
|
|
||||||
SELECT 1 FROM cfdis pi_act
|
|
||||||
WHERE LOWER(pi_act.uuid) = LOWER(r_act.uuid_relacionado)
|
|
||||||
AND pi_act.tipo_comprobante = 'I'
|
|
||||||
AND pi_act.uso_cfdi IN ${ACTIVOS_USOS}
|
|
||||||
))
|
|
||||||
)
|
|
||||||
))
|
))
|
||||||
|
AND NOT (tipo_comprobante = 'E' AND uuid IN (${UUIDS_E_DE_ACTIVOS}))
|
||||||
AND NOT (tipo_comprobante = 'I' AND EXISTS (
|
AND NOT (tipo_comprobante = 'I' AND EXISTS (
|
||||||
-- Anticipo: CFDI tipo I (puede no tener uso_cfdi de activo) que es
|
-- Anticipo: CFDI tipo I (puede no tener uso_cfdi de activo) que es
|
||||||
-- referenciado por una I/07 PPD con uso_cfdi de activo. La I/07 PPD
|
-- referenciado por una I/07 PPD con uso_cfdi de activo. La I/07 PPD
|
||||||
@@ -87,24 +105,10 @@ function activosExclusionAlias(alias: string): string {
|
|||||||
return `
|
return `
|
||||||
AND NOT (${alias}.tipo_comprobante = 'I' AND ${alias}.uso_cfdi IN ${ACTIVOS_USOS})
|
AND NOT (${alias}.tipo_comprobante = 'I' AND ${alias}.uso_cfdi IN ${ACTIVOS_USOS})
|
||||||
AND NOT (${alias}.tipo_comprobante = 'P' AND EXISTS (
|
AND NOT (${alias}.tipo_comprobante = 'P' AND EXISTS (
|
||||||
SELECT 1 FROM cfdis i_act
|
SELECT 1 FROM (${UUIDS_ACTIVOS}) ua
|
||||||
WHERE LOWER(i_act.uuid) = LOWER(${alias}.uuid_relacionado)
|
WHERE ua.uuid = ANY(string_to_array(LOWER(${alias}.uuid_relacionado), '|'))
|
||||||
AND i_act.tipo_comprobante = 'I'
|
|
||||||
AND i_act.uso_cfdi IN ${ACTIVOS_USOS}
|
|
||||||
))
|
|
||||||
AND NOT (${alias}.tipo_comprobante = 'E' AND ${alias}.cfdis_relacionados IS NOT NULL AND EXISTS (
|
|
||||||
SELECT 1 FROM cfdis r_act
|
|
||||||
WHERE LOWER(r_act.uuid) = ANY(string_to_array(LOWER(${alias}.cfdis_relacionados), '|'))
|
|
||||||
AND (
|
|
||||||
(r_act.tipo_comprobante = 'I' AND r_act.uso_cfdi IN ${ACTIVOS_USOS})
|
|
||||||
OR (r_act.tipo_comprobante = 'P' AND EXISTS (
|
|
||||||
SELECT 1 FROM cfdis pi_act
|
|
||||||
WHERE LOWER(pi_act.uuid) = LOWER(r_act.uuid_relacionado)
|
|
||||||
AND pi_act.tipo_comprobante = 'I'
|
|
||||||
AND pi_act.uso_cfdi IN ${ACTIVOS_USOS}
|
|
||||||
))
|
|
||||||
)
|
|
||||||
))
|
))
|
||||||
|
AND NOT (${alias}.tipo_comprobante = 'E' AND ${alias}.uuid IN (${UUIDS_E_DE_ACTIVOS}))
|
||||||
AND NOT (${alias}.tipo_comprobante = 'I' AND EXISTS (
|
AND NOT (${alias}.tipo_comprobante = 'I' AND EXISTS (
|
||||||
SELECT 1 FROM cfdis i07_act
|
SELECT 1 FROM cfdis i07_act
|
||||||
WHERE i07_act.tipo_comprobante = 'I'
|
WHERE i07_act.tipo_comprobante = 'I'
|
||||||
|
|||||||
@@ -96,12 +96,32 @@ export async function getAsignacionesPorSupervisor(
|
|||||||
): Promise<{ obligaciones: AsignacionObligacion[]; tareas: AsignacionTarea[] }> {
|
): Promise<{ obligaciones: AsignacionObligacion[]; tareas: AsignacionTarea[] }> {
|
||||||
const isOwner = role === 'owner' || role === 'cfo' || role === 'contador';
|
const isOwner = role === 'owner' || role === 'cfo' || role === 'contador';
|
||||||
|
|
||||||
|
// Relación supervisor → auxiliar se infiere desde carteras (directas y
|
||||||
|
// subcarteras) con fallback a la tabla legacy auxiliar_supervisores.
|
||||||
|
const supervisorFilter = isOwner
|
||||||
|
? ''
|
||||||
|
: `AND EXISTS (
|
||||||
|
SELECT 1 FROM (
|
||||||
|
SELECT c.auxiliar_user_id
|
||||||
|
FROM carteras c
|
||||||
|
WHERE c.supervisor_user_id = $1
|
||||||
|
AND c.auxiliar_user_id IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT sub.auxiliar_user_id
|
||||||
|
FROM carteras sub
|
||||||
|
JOIN carteras p ON p.id = sub.parent_id
|
||||||
|
WHERE p.supervisor_user_id = $1
|
||||||
|
AND sub.auxiliar_user_id IS NOT NULL
|
||||||
|
UNION
|
||||||
|
SELECT auxiliar_user_id FROM auxiliar_supervisores WHERE supervisor_user_id = $1
|
||||||
|
) sup_aux WHERE sup_aux.auxiliar_user_id = __AUX_COL__
|
||||||
|
)`;
|
||||||
const whereObl = isOwner
|
const whereObl = isOwner
|
||||||
? 'WHERE 1=1'
|
? 'WHERE 1=1'
|
||||||
: 'WHERE EXISTS (SELECT 1 FROM auxiliar_supervisores asp WHERE asp.auxiliar_user_id = oa.auxiliar_user_id AND asp.supervisor_user_id = $1)';
|
: `WHERE 1=1 ${supervisorFilter.replace(/__AUX_COL__/g, 'oa.auxiliar_user_id')}`;
|
||||||
const whereTarea = isOwner
|
const whereTarea = isOwner
|
||||||
? 'WHERE 1=1'
|
? 'WHERE 1=1'
|
||||||
: 'WHERE EXISTS (SELECT 1 FROM auxiliar_supervisores asp WHERE asp.auxiliar_user_id = ta.auxiliar_user_id AND asp.supervisor_user_id = $1)';
|
: `WHERE 1=1 ${supervisorFilter.replace(/__AUX_COL__/g, 'ta.auxiliar_user_id')}`;
|
||||||
const params = isOwner ? [] : [supervisorUserId];
|
const params = isOwner ? [] : [supervisorUserId];
|
||||||
|
|
||||||
const { rows: obligaciones } = await pool.query<AsignacionObligacion>(
|
const { rows: obligaciones } = await pool.query<AsignacionObligacion>(
|
||||||
@@ -301,3 +321,23 @@ export async function getAuxiliarAsignadoTarea(
|
|||||||
const names = await resolveUserNames([auxId]);
|
const names = await resolveUserNames([auxId]);
|
||||||
return { auxiliarUserId: auxId, auxiliarNombre: names.get(auxId) ?? null };
|
return { auxiliarUserId: auxId, auxiliarNombre: names.get(auxId) ?? null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devuelve los userIds de auxiliares que tienen al contribuyente en alguna
|
||||||
|
* de sus subcarteras (carteras con auxiliar_user_id no nulo que contienen
|
||||||
|
* al contribuyente en cartera_entidades).
|
||||||
|
*/
|
||||||
|
export async function getAuxiliaresElegibles(
|
||||||
|
pool: Pool,
|
||||||
|
contribuyenteId: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const { rows } = await pool.query<{ auxiliar_user_id: string }>(
|
||||||
|
`SELECT DISTINCT c.auxiliar_user_id
|
||||||
|
FROM carteras c
|
||||||
|
JOIN cartera_entidades ce ON ce.cartera_id = c.id
|
||||||
|
WHERE ce.entidad_id = $1
|
||||||
|
AND c.auxiliar_user_id IS NOT NULL`,
|
||||||
|
[contribuyenteId],
|
||||||
|
);
|
||||||
|
return rows.map(r => r.auxiliar_user_id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Pool } from 'pg';
|
import type { Pool, PoolClient } from 'pg';
|
||||||
import type { IvaMensual, IsrMensual, ResumenIva, IvaRegimenDetalle, ResumenIsr } from '@horux/shared';
|
import type { IvaMensual, IsrMensual, ResumenIva, IvaRegimenDetalle, ResumenIsr } from '@horux/shared';
|
||||||
import { getRegimenesIgnoradosClaves } from './regimen.service.js';
|
import { getRegimenesIgnoradosClaves } from './regimen.service.js';
|
||||||
import {
|
import {
|
||||||
@@ -106,32 +106,40 @@ const SUM_E_REFERENCING_TRAS = (
|
|||||||
esLadoE: string,
|
esLadoE: string,
|
||||||
considerarActivos: boolean,
|
considerarActivos: boolean,
|
||||||
considerarNCs: boolean,
|
considerarNCs: boolean,
|
||||||
) => `COALESCE((
|
) => {
|
||||||
SELECT SUM(${IVA_TRAS_EXPR_ALIAS('e')})
|
if (!considerarNCs) return '0';
|
||||||
FROM cfdis e
|
return `COALESCE((
|
||||||
WHERE e.tipo_comprobante = 'E'
|
SELECT SUM(${IVA_TRAS_EXPR_ALIAS('e')})
|
||||||
AND e.metodo_pago = 'PUE'
|
FROM cfdis e
|
||||||
AND e.status NOT IN ('Cancelado', '0')
|
WHERE e.tipo_comprobante = 'E'
|
||||||
AND ${esLadoE}
|
AND e.metodo_pago = 'PUE'
|
||||||
AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
|
AND e.status NOT IN ('Cancelado', '0')
|
||||||
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
|
AND ${esLadoE}
|
||||||
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
|
AND e.cfdis_relacionados IS NOT NULL
|
||||||
), 0)`;
|
AND string_to_array(LOWER(e.cfdis_relacionados), '|') @> ARRAY[LOWER(cfdis.uuid)]
|
||||||
|
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
|
||||||
|
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
|
||||||
|
), 0)`;
|
||||||
|
};
|
||||||
const SUM_E_REFERENCING_RET = (
|
const SUM_E_REFERENCING_RET = (
|
||||||
esLadoE: string,
|
esLadoE: string,
|
||||||
considerarActivos: boolean,
|
considerarActivos: boolean,
|
||||||
considerarNCs: boolean,
|
considerarNCs: boolean,
|
||||||
) => `COALESCE((
|
) => {
|
||||||
SELECT SUM(${IVA_RET_EXPR_ALIAS('e')})
|
if (!considerarNCs) return '0';
|
||||||
FROM cfdis e
|
return `COALESCE((
|
||||||
WHERE e.tipo_comprobante = 'E'
|
SELECT SUM(${IVA_RET_EXPR_ALIAS('e')})
|
||||||
AND e.metodo_pago = 'PUE'
|
FROM cfdis e
|
||||||
AND e.status NOT IN ('Cancelado', '0')
|
WHERE e.tipo_comprobante = 'E'
|
||||||
AND ${esLadoE}
|
AND e.metodo_pago = 'PUE'
|
||||||
AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
|
AND e.status NOT IN ('Cancelado', '0')
|
||||||
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
|
AND ${esLadoE}
|
||||||
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
|
AND e.cfdis_relacionados IS NOT NULL
|
||||||
), 0)`;
|
AND string_to_array(LOWER(e.cfdis_relacionados), '|') @> ARRAY[LOWER(cfdis.uuid)]
|
||||||
|
AND date_trunc('month', COALESCE(e.fecha_efectiva, e.fecha_emision - interval '1 hour'))
|
||||||
|
= date_trunc('month', COALESCE(cfdis.fecha_efectiva, cfdis.fecha_emision - interval '1 hour'))${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
|
||||||
|
), 0)`;
|
||||||
|
};
|
||||||
// Régimen del contribuyente según su lado: emisor/receptor del CFDI.
|
// Régimen del contribuyente según su lado: emisor/receptor del CFDI.
|
||||||
// Usa el RFC del contribuyente (via `ctx.esEmisor`/`ctx.esReceptor`) para
|
// Usa el RFC del contribuyente (via `ctx.esEmisor`/`ctx.esReceptor`) para
|
||||||
// determinar el lado, no el `type` de BD.
|
// determinar el lado, no el `type` de BD.
|
||||||
@@ -152,16 +160,20 @@ const HAS_E_REFERENCING_MISMO_MES = (
|
|||||||
esLadoE: string,
|
esLadoE: string,
|
||||||
considerarActivos: boolean,
|
considerarActivos: boolean,
|
||||||
considerarNCs: boolean,
|
considerarNCs: boolean,
|
||||||
) => `EXISTS (
|
) => {
|
||||||
SELECT 1 FROM cfdis e
|
if (!considerarNCs) return 'FALSE';
|
||||||
WHERE e.tipo_comprobante = 'E'
|
return `EXISTS (
|
||||||
AND e.metodo_pago = 'PUE'
|
SELECT 1 FROM cfdis e
|
||||||
AND e.status NOT IN ('Cancelado', '0')
|
WHERE e.tipo_comprobante = 'E'
|
||||||
AND ${esLadoE}
|
AND e.metodo_pago = 'PUE'
|
||||||
AND LOWER(cfdis.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
|
AND e.status NOT IN ('Cancelado', '0')
|
||||||
AND date_trunc('month', e.fecha_emision)
|
AND ${esLadoE}
|
||||||
= date_trunc('month', cfdis.fecha_emision)${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
|
AND e.cfdis_relacionados IS NOT NULL
|
||||||
)`;
|
AND string_to_array(LOWER(e.cfdis_relacionados), '|') @> ARRAY[LOWER(cfdis.uuid)]
|
||||||
|
AND date_trunc('month', e.fecha_emision)
|
||||||
|
= date_trunc('month', cfdis.fecha_emision)${buildExtraFiltersAlias('e', considerarActivos, considerarNCs)}
|
||||||
|
)`;
|
||||||
|
};
|
||||||
|
|
||||||
// Atribución por lado usando RFC en lugar de `type`. Los buckets son
|
// Atribución por lado usando RFC en lugar de `type`. Los buckets son
|
||||||
// factories que reciben el context del contribuyente:
|
// factories que reciben el context del contribuyente:
|
||||||
@@ -397,8 +409,8 @@ export async function getIvaMensual(
|
|||||||
const añoEnd = `${año}-12-31`;
|
const añoEnd = `${año}-12-31`;
|
||||||
const extra = buildExtraFilters(considerarActivos, considerarNCs);
|
const extra = buildExtraFilters(considerarActivos, considerarNCs);
|
||||||
|
|
||||||
const [{ rows: causadoRows }, { rows: acreditableRows }] = await Promise.all([
|
const { rows: causadoRows } = await withJitOff(pool, (client) =>
|
||||||
pool.query<{ mes: number; trasladado: string; retencion: string }>(`
|
client.query<{ mes: number; trasladado: string; retencion: string }>(`
|
||||||
SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
|
SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
|
||||||
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
||||||
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
||||||
@@ -407,8 +419,10 @@ export async function getIvaMensual(
|
|||||||
AND ${VIGENTE} AND ${FR}${extra}
|
AND ${VIGENTE} AND ${FR}${extra}
|
||||||
AND (${REGIMEN_TENANT}) = ANY($3)
|
AND (${REGIMEN_TENANT}) = ANY($3)
|
||||||
GROUP BY mes
|
GROUP BY mes
|
||||||
`, [añoStart, añoEnd, TODOS_REGIMENES]),
|
`, [añoStart, añoEnd, TODOS_REGIMENES])
|
||||||
pool.query<{ mes: number; trasladado: string; retencion: string }>(`
|
);
|
||||||
|
const { rows: acreditableRows } = await withJitOff(pool, (client) =>
|
||||||
|
client.query<{ mes: number; trasladado: string; retencion: string }>(`
|
||||||
SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
|
SELECT EXTRACT(MONTH FROM ${FECHA_EFECTIVA})::int as mes,
|
||||||
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
||||||
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
||||||
@@ -417,8 +431,8 @@ export async function getIvaMensual(
|
|||||||
AND ${VIGENTE} AND ${FR}${extra}
|
AND ${VIGENTE} AND ${FR}${extra}
|
||||||
AND (${REGIMEN_TENANT}) = ANY($3)
|
AND (${REGIMEN_TENANT}) = ANY($3)
|
||||||
GROUP BY mes
|
GROUP BY mes
|
||||||
`, [añoStart, añoEnd, TODOS_REGIMENES]),
|
`, [añoStart, añoEnd, TODOS_REGIMENES])
|
||||||
]);
|
);
|
||||||
|
|
||||||
perMes = new Map();
|
perMes = new Map();
|
||||||
for (const row of causadoRows) {
|
for (const row of causadoRows) {
|
||||||
@@ -648,20 +662,22 @@ async function readResumenIvaFromCache(
|
|||||||
const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
|
const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
|
||||||
const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
|
const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
|
||||||
const REGIMEN_TENANT = regimenTenantExpr(ctx);
|
const REGIMEN_TENANT = regimenTenantExpr(ctx);
|
||||||
const acumRow = (await pool.query(`
|
const acumRow = (await withJitOff(pool, (client) =>
|
||||||
SELECT
|
client.query(`
|
||||||
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
SELECT
|
||||||
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
||||||
(
|
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
||||||
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
|
(
|
||||||
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
|
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
|
||||||
) as total
|
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
|
||||||
FROM cfdis
|
) as total
|
||||||
WHERE ${VIGENTE}
|
FROM cfdis
|
||||||
AND (${REGIMEN_TENANT}) = ANY($3)
|
WHERE ${VIGENTE}
|
||||||
AND ${acumFR}
|
AND (${REGIMEN_TENANT}) = ANY($3)
|
||||||
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
|
AND ${acumFR}
|
||||||
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES])).rows[0];
|
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
|
||||||
|
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES])
|
||||||
|
)).rows[0];
|
||||||
|
|
||||||
// Cache hit retorna 0/empty para los surface IVA No Acreditable. El cache
|
// Cache hit retorna 0/empty para los surface IVA No Acreditable. El cache
|
||||||
// aún no persiste esos campos — si se hace crítico para BI, agregar columna
|
// aún no persiste esos campos — si se hace crítico para BI, agregar columna
|
||||||
@@ -698,6 +714,29 @@ async function readResumenIvaFromCache(
|
|||||||
*
|
*
|
||||||
* Algebraicamente: T − A − R == dashboard.balance, céntimo por céntimo.
|
* Algebraicamente: T − A − R == dashboard.balance, céntimo por céntimo.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Ejecuta un callback con un client de pool con JIT desactivado (SET LOCAL jit = off).
|
||||||
|
* Usa una transacción implícita para que el SET LOCAL se restaure automáticamente
|
||||||
|
* al liberar la conexión. Esto evita que PostgreSQL compile JIT para queries con
|
||||||
|
* muchos subplans (correlacionados), lo cual puede tardar >15s en queries con
|
||||||
|
* costo estimado muy alto aunque la ejecución real sea rápida.
|
||||||
|
*/
|
||||||
|
async function withJitOff<T>(pool: Pool, fn: (client: PoolClient) => Promise<T>): Promise<T> {
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
await client.query('SET LOCAL jit = off');
|
||||||
|
const result = await fn(client);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
await client.query('ROLLBACK').catch(() => {});
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getResumenIva(
|
export async function getResumenIva(
|
||||||
pool: Pool,
|
pool: Pool,
|
||||||
fechaInicio: string,
|
fechaInicio: string,
|
||||||
@@ -725,10 +764,10 @@ export async function getResumenIva(
|
|||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Una query por lado (causado / acreditable). Filtro por RFC via
|
// Queries con JIT off: evitan compilación JIT de >15s en queries con muchos
|
||||||
// ctx.esEmisor/esReceptor (embedded en buckets/signed exprs).
|
// subplans correlacionados (activado por costo estimado >100k).
|
||||||
const [{ rows: causadoRows }, { rows: acreditableRows }] = await Promise.all([
|
const { rows: causadoRows } = await withJitOff(pool, (client) =>
|
||||||
pool.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
|
client.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
|
||||||
SELECT ${REGIMEN_TENANT} as regimen,
|
SELECT ${REGIMEN_TENANT} as regimen,
|
||||||
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
||||||
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
||||||
@@ -737,8 +776,10 @@ export async function getResumenIva(
|
|||||||
AND ${VIGENTE} AND ${FR}${extra}
|
AND ${VIGENTE} AND ${FR}${extra}
|
||||||
AND (${REGIMEN_TENANT}) = ANY($3)
|
AND (${REGIMEN_TENANT}) = ANY($3)
|
||||||
GROUP BY ${REGIMEN_TENANT}
|
GROUP BY ${REGIMEN_TENANT}
|
||||||
`, [fechaInicio, fechaFin, TODOS_REGIMENES]),
|
`, [fechaInicio, fechaFin, TODOS_REGIMENES])
|
||||||
pool.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
|
);
|
||||||
|
const { rows: acreditableRows } = await withJitOff(pool, (client) =>
|
||||||
|
client.query<{ regimen: string | null; trasladado: string; retencion: string }>(`
|
||||||
SELECT ${REGIMEN_TENANT} as regimen,
|
SELECT ${REGIMEN_TENANT} as regimen,
|
||||||
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) as trasladado,
|
||||||
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0) as retencion
|
||||||
@@ -747,8 +788,8 @@ export async function getResumenIva(
|
|||||||
AND ${VIGENTE} AND ${FR}${extra}
|
AND ${VIGENTE} AND ${FR}${extra}
|
||||||
AND (${REGIMEN_TENANT}) = ANY($3)
|
AND (${REGIMEN_TENANT}) = ANY($3)
|
||||||
GROUP BY ${REGIMEN_TENANT}
|
GROUP BY ${REGIMEN_TENANT}
|
||||||
`, [fechaInicio, fechaFin, TODOS_REGIMENES]),
|
`, [fechaInicio, fechaFin, TODOS_REGIMENES])
|
||||||
]);
|
);
|
||||||
|
|
||||||
// Combinar por régimen: el set de régimenes posibles es la unión de ambos lados.
|
// Combinar por régimen: el set de régimenes posibles es la unión de ambos lados.
|
||||||
type Acc = { trasCausado: number; retCausado: number; trasAcreditable: number; retAcreditable: number };
|
type Acc = { trasCausado: number; retCausado: number; trasAcreditable: number; retAcreditable: number };
|
||||||
@@ -799,20 +840,22 @@ export async function getResumenIva(
|
|||||||
// Acumulado anual (misma fórmula T − A − R, pero rango = enero → fechaFin).
|
// Acumulado anual (misma fórmula T − A − R, pero rango = enero → fechaFin).
|
||||||
const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
|
const añoInicio = new Date(fechaInicio + 'T00:00:00').getFullYear();
|
||||||
const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
|
const acumFR = conciliacion ? FECHA_RANGO_CONCILIACION : FECHA_RANGO;
|
||||||
const { rows: [acumRow] } = await pool.query(`
|
const { rows: [acumRow] } = await withJitOff(pool, (client) =>
|
||||||
SELECT
|
client.query(`
|
||||||
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
SELECT
|
||||||
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
COALESCE(SUM(${signedCausadoTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
||||||
(
|
COALESCE(SUM(${signedAcreditableTras(ctx, considerarActivos, considerarNCs)}), 0) -
|
||||||
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
|
(
|
||||||
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
|
COALESCE(SUM(${signedCausadoRet(ctx, considerarActivos, considerarNCs)}), 0) -
|
||||||
) as total
|
COALESCE(SUM(${signedAcreditableRet(ctx, considerarActivos, considerarNCs)}), 0)
|
||||||
FROM cfdis
|
) as total
|
||||||
WHERE ${VIGENTE}
|
FROM cfdis
|
||||||
AND (${REGIMEN_TENANT}) = ANY($3)
|
WHERE ${VIGENTE}
|
||||||
AND ${acumFR}${extra}
|
AND (${REGIMEN_TENANT}) = ANY($3)
|
||||||
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
|
AND ${acumFR}${extra}
|
||||||
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES]);
|
AND (${ctx.esEmisor} OR ${ctx.esReceptor})
|
||||||
|
`, [`${añoInicio}-01-01`, fechaFin, TODOS_REGIMENES])
|
||||||
|
);
|
||||||
|
|
||||||
// IVA No Acreditable surface (Art. 5 LIVA fracción I + Art. 27 fracción III LISR).
|
// IVA No Acreditable surface (Art. 5 LIVA fracción I + Art. 27 fracción III LISR).
|
||||||
// No participa en `resultado` — ya excluido del `acreditable` arriba via filtro
|
// No participa en `resultado` — ya excluido del `acreditable` arriba via filtro
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
useDesasignarObligacion,
|
useDesasignarObligacion,
|
||||||
useAsignarTarea,
|
useAsignarTarea,
|
||||||
useDesasignarTarea,
|
useDesasignarTarea,
|
||||||
|
useAuxiliaresElegibles,
|
||||||
} from '@/lib/hooks/use-asignaciones';
|
} from '@/lib/hooks/use-asignaciones';
|
||||||
import { useUsuarios } from '@/lib/hooks/use-usuarios';
|
import { useUsuarios } from '@/lib/hooks/use-usuarios';
|
||||||
import { useAuthStore } from '@/stores/auth-store';
|
import { useAuthStore } from '@/stores/auth-store';
|
||||||
@@ -36,6 +37,11 @@ export default function SeguimientoAuxiliares() {
|
|||||||
|
|
||||||
const auxiliares = (usuarios ?? []).filter((u: any) => u.role === 'auxiliar');
|
const auxiliares = (usuarios ?? []).filter((u: any) => u.role === 'auxiliar');
|
||||||
|
|
||||||
|
const { data: elegiblesData, isLoading: loadingElegibles } = useAuxiliaresElegibles(modalItem?.contribuyenteId);
|
||||||
|
const auxiliaresIdsElegibles = elegiblesData?.auxiliares ?? [];
|
||||||
|
const auxiliaresFiltrados = auxiliares.filter((a: any) => auxiliaresIdsElegibles.includes(a.id));
|
||||||
|
const puedeAsignar = !loadingElegibles && auxiliaresFiltrados.length > 0;
|
||||||
|
|
||||||
const openAssignModal = (type: 'obligacion' | 'tarea', item: any) => {
|
const openAssignModal = (type: 'obligacion' | 'tarea', item: any) => {
|
||||||
setModalType(type);
|
setModalType(type);
|
||||||
setModalItem(item);
|
setModalItem(item);
|
||||||
@@ -169,20 +175,28 @@ export default function SeguimientoAuxiliares() {
|
|||||||
<p className="text-xs text-muted-foreground mb-4">
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
Contribuyente: {modalItem?.contribuyenteRazonSocial} ({modalItem?.contribuyenteRfc})
|
Contribuyente: {modalItem?.contribuyenteRazonSocial} ({modalItem?.contribuyenteRfc})
|
||||||
</p>
|
</p>
|
||||||
<Select value={selectedAuxiliar} onValueChange={setSelectedAuxiliar}>
|
{loadingElegibles ? (
|
||||||
<SelectTrigger>
|
<p className="text-sm text-muted-foreground">Verificando subcarteras...</p>
|
||||||
<SelectValue placeholder="Selecciona un auxiliar" />
|
) : auxiliaresFiltrados.length === 0 ? (
|
||||||
</SelectTrigger>
|
<p className="text-sm text-red-600">
|
||||||
<SelectContent>
|
Ningún auxiliar tiene este contribuyente en su subcartera. No se puede asignar.
|
||||||
{auxiliares.map((a: any) => (
|
</p>
|
||||||
<SelectItem key={a.id} value={a.id}>{a.nombre} ({a.email})</SelectItem>
|
) : (
|
||||||
))}
|
<Select value={selectedAuxiliar} onValueChange={setSelectedAuxiliar}>
|
||||||
</SelectContent>
|
<SelectTrigger>
|
||||||
</Select>
|
<SelectValue placeholder="Selecciona un auxiliar" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{auxiliaresFiltrados.map((a: any) => (
|
||||||
|
<SelectItem key={a.id} value={a.id}>{a.nombre} ({a.email})</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setModalOpen(false)}>Cancelar</Button>
|
<Button variant="outline" onClick={() => setModalOpen(false)}>Cancelar</Button>
|
||||||
<Button onClick={handleAssign} disabled={!selectedAuxiliar}>
|
<Button onClick={handleAssign} disabled={!selectedAuxiliar || !puedeAsignar}>
|
||||||
{modalItem?.auxiliarUserId ? 'Reasignar' : 'Asignar'}
|
{modalItem?.auxiliarUserId ? 'Reasignar' : 'Asignar'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ export default function CfdiPage() {
|
|||||||
'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision),
|
'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision),
|
||||||
'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante),
|
'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante),
|
||||||
'Uso CFDI': (cfdi as any).usoCfdi || '',
|
'Uso CFDI': (cfdi as any).usoCfdi || '',
|
||||||
|
'Forma de Pago': cfdi.formaPago || '',
|
||||||
'Serie': cfdi.serie || '',
|
'Serie': cfdi.serie || '',
|
||||||
'Folio': cfdi.folio || '',
|
'Folio': cfdi.folio || '',
|
||||||
'RFC Emisor': cfdi.rfcEmisor,
|
'RFC Emisor': cfdi.rfcEmisor,
|
||||||
@@ -541,6 +542,7 @@ export default function CfdiPage() {
|
|||||||
'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision),
|
'Fecha Emisión': formatCfdiDate(cfdi.fechaEmision),
|
||||||
'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante),
|
'Tipo Comprobante': formatTipoComprobante(cfdi.tipoComprobante),
|
||||||
'Uso CFDI': (cfdi as any).usoCfdi || '',
|
'Uso CFDI': (cfdi as any).usoCfdi || '',
|
||||||
|
'Forma de Pago': cfdi.formaPago || '',
|
||||||
'Serie': cfdi.serie || '',
|
'Serie': cfdi.serie || '',
|
||||||
'Folio': cfdi.folio || '',
|
'Folio': cfdi.folio || '',
|
||||||
'RFC Emisor': cfdi.rfcEmisor,
|
'RFC Emisor': cfdi.rfcEmisor,
|
||||||
@@ -1699,6 +1701,7 @@ export default function CfdiPage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</th>
|
</th>
|
||||||
|
<th className="pb-3 font-medium"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="text-sm text-center">
|
<tbody className="text-sm text-center">
|
||||||
@@ -1715,6 +1718,21 @@ export default function CfdiPage() {
|
|||||||
<td className="py-2 text-xs" title={row.unidad || ''}>{row.clave_unidad || '-'}</td>
|
<td className="py-2 text-xs" title={row.unidad || ''}>{row.clave_unidad || '-'}</td>
|
||||||
<td className="py-2 text-right">${Number(row.valor_unitario ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>
|
<td className="py-2 text-right">${Number(row.valor_unitario ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>
|
||||||
<td className="py-2 text-right font-medium">${Number(row.importe ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>
|
<td className="py-2 text-right font-medium">${Number(row.importe ?? 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}</td>
|
||||||
|
<td className="py-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleViewCfdi(row.cfdi_id)}
|
||||||
|
disabled={loadingCfdi === row.cfdi_id}
|
||||||
|
title="Ver CFDI"
|
||||||
|
>
|
||||||
|
{loadingCfdi === row.cfdi_id ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ export default function ContribuyentesPage() {
|
|||||||
setShowDialog(true);
|
setShowDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canCreate = user?.role === 'owner' || user?.role === 'cfo' || user?.role === 'supervisor';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6 max-w-7xl mx-auto">
|
<div className="p-6 space-y-6 max-w-7xl mx-auto">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -95,14 +97,16 @@ export default function ContribuyentesPage() {
|
|||||||
<h1 className="text-2xl font-bold">Contribuyentes</h1>
|
<h1 className="text-2xl font-bold">Contribuyentes</h1>
|
||||||
<p className="text-sm text-muted-foreground">RFCs que gestiona tu despacho · {rfcCounterText}</p>
|
<p className="text-sm text-muted-foreground">RFCs que gestiona tu despacho · {rfcCounterText}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{canCreate && (
|
||||||
onClick={() => { resetForm(); setShowDialog(true); }}
|
<Button
|
||||||
disabled={trialAtLimit}
|
onClick={() => { resetForm(); setShowDialog(true); }}
|
||||||
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
|
disabled={trialAtLimit}
|
||||||
className="flex items-center gap-2"
|
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
|
||||||
>
|
className="flex items-center gap-2"
|
||||||
<Plus className="h-4 w-4" /> Agregar RFC
|
>
|
||||||
</Button>
|
<Plus className="h-4 w-4" /> Agregar RFC
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? <p className="text-muted-foreground">Cargando...</p> : !contribuyentes || contribuyentes.length === 0 ? (
|
{isLoading ? <p className="text-muted-foreground">Cargando...</p> : !contribuyentes || contribuyentes.length === 0 ? (
|
||||||
@@ -110,13 +114,15 @@ export default function ContribuyentesPage() {
|
|||||||
<Building2 className="h-12 w-12 text-muted-foreground mb-4" />
|
<Building2 className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
<h3 className="text-lg font-semibold">Sin contribuyentes</h3>
|
<h3 className="text-lg font-semibold">Sin contribuyentes</h3>
|
||||||
<p className="text-sm text-muted-foreground mt-1 mb-4">Agrega el primer RFC para empezar.</p>
|
<p className="text-sm text-muted-foreground mt-1 mb-4">Agrega el primer RFC para empezar.</p>
|
||||||
<Button
|
{canCreate && (
|
||||||
onClick={() => { resetForm(); setShowDialog(true); }}
|
<Button
|
||||||
disabled={trialAtLimit}
|
onClick={() => { resetForm(); setShowDialog(true); }}
|
||||||
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
|
disabled={trialAtLimit}
|
||||||
>
|
title={trialAtLimit ? TRIAL_LIMIT_TOOLTIP : undefined}
|
||||||
Agregar primer RFC
|
>
|
||||||
</Button>
|
Agregar primer RFC
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</CardContent></Card>
|
</CardContent></Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-3 lg:grid-cols-2 3xl:grid-cols-3 4xl:grid-cols-4">{contribuyentes.map((c) => (
|
<div className="grid gap-3 lg:grid-cols-2 3xl:grid-cols-3 4xl:grid-cols-4">{contribuyentes.map((c) => (
|
||||||
@@ -127,9 +133,13 @@ export default function ContribuyentesPage() {
|
|||||||
{c.regimenFiscal && <p className="text-xs text-muted-foreground mt-1">Régimen: {c.regimenFiscal}</p>}
|
{c.regimenFiscal && <p className="text-xs text-muted-foreground mt-1">Régimen: {c.regimenFiscal}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="ghost" size="sm" onClick={() => setAddonsTarget({ id: c.id, nombre: c.nombre })} title="Add-ons"><Sparkles className="h-4 w-4" /></Button>
|
{(user?.role === 'owner' || user?.role === 'cfo') && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setAddonsTarget({ id: c.id, nombre: c.nombre })} title="Add-ons"><Sparkles className="h-4 w-4" /></Button>
|
||||||
|
)}
|
||||||
<Button variant="ghost" size="sm" onClick={() => openEdit(c)}><Pencil className="h-4 w-4" /></Button>
|
<Button variant="ghost" size="sm" onClick={() => openEdit(c)}><Pencil className="h-4 w-4" /></Button>
|
||||||
<Button variant="ghost" size="sm" onClick={() => handleDeactivate(c.id, c.rfc)} className="text-destructive hover:text-destructive"><Trash2 className="h-4 w-4" /></Button>
|
{user?.role === 'owner' && (
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => handleDeactivate(c.id, c.rfc)} className="text-destructive hover:text-destructive"><Trash2 className="h-4 w-4" /></Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent></Card>
|
</CardContent></Card>
|
||||||
))}</div>
|
))}</div>
|
||||||
|
|||||||
@@ -56,3 +56,6 @@ export const asignarTarea = (tareaId: string, auxiliarUserId: string) =>
|
|||||||
|
|
||||||
export const desasignarTarea = (tareaId: string) =>
|
export const desasignarTarea = (tareaId: string) =>
|
||||||
apiClient.delete(`/tareas/${tareaId}/asignar`).then(r => r.data);
|
apiClient.delete(`/tareas/${tareaId}/asignar`).then(r => r.data);
|
||||||
|
|
||||||
|
export const getAuxiliaresElegibles = (contribuyenteId: string) =>
|
||||||
|
apiClient.get<{ auxiliares: string[] }>(`/carteras/asignaciones/auxiliares-elegibles/${contribuyenteId}`).then(r => r.data);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
desasignarObligacion,
|
desasignarObligacion,
|
||||||
asignarTarea,
|
asignarTarea,
|
||||||
desasignarTarea,
|
desasignarTarea,
|
||||||
|
getAuxiliaresElegibles,
|
||||||
} from '../api/asignaciones';
|
} from '../api/asignaciones';
|
||||||
|
|
||||||
export function useAsignacionesSupervisor() {
|
export function useAsignacionesSupervisor() {
|
||||||
@@ -87,3 +88,11 @@ export function useDesasignarTarea() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useAuxiliaresElegibles(contribuyenteId: string | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['auxiliares-elegibles', contribuyenteId],
|
||||||
|
queryFn: () => getAuxiliaresElegibles(contribuyenteId!),
|
||||||
|
enabled: !!contribuyenteId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user