Update: nueva version Horux Despachos
This commit is contained in:
491
docs/plans/2026-04-26-i07-ppd-compensacion.md
Normal file
491
docs/plans/2026-04-26-i07-ppd-compensacion.md
Normal file
@@ -0,0 +1,491 @@
|
||||
# Sesión 2026-04-26 — Compensación I/07 PPD + Activos Fijos
|
||||
|
||||
Fix focalizado: cuando una I/07 PPD aplica un anticipo y en el **mismo
|
||||
mes/año** existe una E (cualquier TipoRelacion) que referencia esa
|
||||
I/07 PPD, la I/07 PPD aporta al bucket = base de la E. Antes el filtro
|
||||
`metodo_pago = 'PUE'` excluía la I/07 PPD del bucket de facturas pero
|
||||
la E sí entraba como NC, generando **gasto/ingreso negativo** en el
|
||||
periodo.
|
||||
|
||||
---
|
||||
|
||||
## 1. Caso real que motivó el fix
|
||||
|
||||
Husberto Ignacio Torres (TOAH680201RA2), agosto-2025, gastos:
|
||||
|
||||
| CFDI | Total | IVA | Base | Notas |
|
||||
|---|---:|---:|---:|---|
|
||||
| Anticipo `729109FC…` | ? | ? | ? | no en BD del tenant |
|
||||
| **I/07 PPD `5c874749`** | $454,000 | $62,621 | $391,379 | apunta al anticipo |
|
||||
| **E/07 PUE `7163da3b`** | $148,000 | $20,414 | $127,586 | apunta a `5c874749` (mismo día 2025-08-08) |
|
||||
| **E/01 PUE `7aac715b`** | $10,000 | $1,379 | $8,621 | también apunta a `5c874749` (mismo día) |
|
||||
|
||||
Patrón observado en BD:
|
||||
- La I/07 PPD apunta al **anticipo** original.
|
||||
- La E (07 o 01) apunta a la **I/07 PPD** (no al anticipo).
|
||||
|
||||
### Comportamiento previo (Método A puro)
|
||||
|
||||
```
|
||||
I/07 PPD → NO entra al bucket (filtro metodo_pago='PUE')
|
||||
E/07 PUE → −$127,586 (NC normal en Método A)
|
||||
E/01 PUE → −$8,621
|
||||
Net agosto-2025 = −$136,207 ❌ (gasto negativo)
|
||||
```
|
||||
|
||||
El anticipo aportó en su periodo (vía P/PUE original), pero al cancelar
|
||||
con la E sin que la I/07 PPD haya entrado al universo del bucket, queda
|
||||
una entrada negativa fantasma.
|
||||
|
||||
### Comportamiento nuevo (con compensación)
|
||||
|
||||
```
|
||||
I/07 PPD compensada = +$127,586 + $8,621 = +$136,207
|
||||
E/07 PUE = −$127,586
|
||||
E/01 PUE = −$8,621
|
||||
Net agosto-2025 = $0 ✓
|
||||
```
|
||||
|
||||
El neto en agosto-2025 vuelve a 0 (el anticipo ya se contó antes y los
|
||||
pagos P futuros materializarán el resto del servicio cuando lleguen).
|
||||
|
||||
---
|
||||
|
||||
## 2. Volumen del patrón en BD
|
||||
|
||||
Búsqueda con el query nuevo (`scripts/find-i07-ppd-cases.ts` filtro
|
||||
RFC):
|
||||
|
||||
| Contribuyente | I/07 PPD ↔ E referencias directas | Mismo mes/año |
|
||||
|---|---:|---:|
|
||||
| Husberto (TOAH680201RA2) | 26 | **23** |
|
||||
| (resto del tenant) | varios | varios |
|
||||
|
||||
23 casos cumplen exactamente la regla "mismo mes/año" en Husberto.
|
||||
Implementarlo afecta de forma medible el dashboard.
|
||||
|
||||
---
|
||||
|
||||
## 3. Implementación
|
||||
|
||||
### Archivos modificados
|
||||
|
||||
`apps/api/src/services/dashboard.service.ts`:
|
||||
|
||||
#### `calcularEgresosPorRegimen` — bucket adicional `i07PpdComp`
|
||||
|
||||
```sql
|
||||
SELECT i.regimen_fiscal_receptor AS regimen,
|
||||
COALESCE(SUM((
|
||||
SELECT COALESCE(SUM(
|
||||
COALESCE(e.total_mxn, 0)
|
||||
- COALESCE(e.iva_traslado_mxn, 0)
|
||||
- COALESCE(e.ieps_traslado_mxn, 0)
|
||||
- COALESCE(e.impuestos_locales_trasladado_mxn, 0)
|
||||
), 0)
|
||||
FROM cfdis e
|
||||
WHERE e.tipo_comprobante = 'E'
|
||||
AND e.status NOT IN ('Cancelado','0')
|
||||
AND ${esReceptorE} -- alias 'e.'
|
||||
AND LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
|
||||
AND date_trunc('month', e.fecha_emision) = date_trunc('month', i.fecha_emision)
|
||||
)), 0) AS monto
|
||||
FROM cfdis i
|
||||
WHERE ${esReceptorI} -- alias 'i.'
|
||||
AND i.tipo_comprobante = 'I' AND i.metodo_pago = 'PPD'
|
||||
AND i.cfdi_tipo_relacion = '07'
|
||||
AND i.status NOT IN ('Cancelado', '0')
|
||||
AND ${FR.replace('fecha_emision', 'i.fecha_emision')}
|
||||
AND i.regimen_fiscal_receptor = ANY($3)
|
||||
GROUP BY i.regimen_fiscal_receptor
|
||||
```
|
||||
|
||||
Sumado al bucket de gastos:
|
||||
```ts
|
||||
const monto = montoF + montoP + montoI07Comp - montoNC;
|
||||
```
|
||||
|
||||
#### `calcularIngresosPorRegimen` Grupo 1 — bucket simétrico `g1I07PpdComp`
|
||||
|
||||
Misma lógica pero del lado **emisor** (`esEmisor` en lugar de
|
||||
`esReceptor`, `regimen_fiscal_emisor`, filtro a `GRUPO_PF_EMPRESARIAL`).
|
||||
|
||||
### Helpers SQL
|
||||
|
||||
Para usar `esEmisor`/`esReceptor` con alias en la query, se hace
|
||||
`replace` inline:
|
||||
|
||||
```ts
|
||||
const esReceptorE = esReceptor.replace(/\brfc_receptor\b/g, 'e.rfc_receptor');
|
||||
```
|
||||
|
||||
`esReceptor` viene de `resolveContribuyenteContext()` como fragmento
|
||||
`UPPER(rfc_receptor) = 'X_RFC'`. El replace lo prepara para usar con el
|
||||
alias `e.`.
|
||||
|
||||
### Lo que NO se tocó
|
||||
|
||||
- **Adquisiciones G01** (`calcularAdquisicionesMercancias`): no se
|
||||
agregó la compensación todavía. Si surge un caso, replicar el patrón
|
||||
con `WHERE e.uso_cfdi = 'G01'` adicional.
|
||||
- **IVA causado/acreditable** (`impuestos.service.ts`): mantiene
|
||||
compensación NETO_CUSTOM con E/07 (no Método A). La regla de I/07 PPD
|
||||
↔ E mismo mes podría aplicar también en simetría, pero requiere
|
||||
análisis por separado y está fuera de este cambio.
|
||||
|
||||
---
|
||||
|
||||
## 4. Validación
|
||||
|
||||
### Typecheck
|
||||
✅ 0 errores en API.
|
||||
|
||||
### Recompute
|
||||
- 212 filas en `metricas_mensuales` invalidadas con razón
|
||||
`I07_PPD_COMPENSACION_E_MISMO_MES`.
|
||||
- 392 filas escritas tras `processAllTenantsInvalidations()`.
|
||||
- 0 errores.
|
||||
|
||||
### Caso de validación
|
||||
Husberto agosto-2025 gastos: el balance −$136,207 generado por las E
|
||||
sin compensación debe desaparecer y volver a 0 en ese periodo.
|
||||
|
||||
---
|
||||
|
||||
## 5. Trade-offs y decisiones documentadas
|
||||
|
||||
### Solo "mismo mes/año"
|
||||
|
||||
La regla del user es "máximo un periodo después". En BD real, ningún
|
||||
caso de Husberto tiene E "1 mes después" — todos los 23 casos están en
|
||||
el mismo mes que su I/07 PPD. La regla `date_trunc('month', e.fecha)
|
||||
= date_trunc('month', i.fecha)` cubre los casos reales.
|
||||
|
||||
Si en el futuro aparecen E un mes después con monto significativo, se
|
||||
puede ampliar a `date_trunc('month', e.fecha) BETWEEN
|
||||
date_trunc('month', i.fecha) AND date_trunc('month', i.fecha + interval
|
||||
'1 month')`.
|
||||
|
||||
### Cualquier TipoRelacion en la E
|
||||
|
||||
La regla original era E/07 (cancelación de anticipo). Pero los casos
|
||||
reales muestran E/01 también compartiendo `cfdis_relacionados` con la
|
||||
I/07 PPD (ej. `7aac715b`). Esto es congruente con el bug "TipoRelacion
|
||||
sospechoso" que ya documentamos: el emisor a veces pone 01 cuando
|
||||
debería ser 07. La compensación nueva captura ambos correctamente.
|
||||
|
||||
### Patrón de referencia: E → I/07 PPD (no E → anticipo)
|
||||
|
||||
El patrón observado en BD muestra que las E referencian a la I/07 PPD
|
||||
directamente. Es el patrón SAT estándar (la E "ajusta" la factura, no
|
||||
el anticipo). El JOIN se hace por:
|
||||
|
||||
```sql
|
||||
LOWER(i.uuid) = ANY(string_to_array(LOWER(e.cfdis_relacionados), '|'))
|
||||
```
|
||||
|
||||
Si en el futuro aparecieran casos con E → anticipo (otro patrón), se
|
||||
puede hacer un UNION con el join alternativo.
|
||||
|
||||
---
|
||||
|
||||
## 6. Pendientes derivados
|
||||
|
||||
- **Validar Husberto agosto-2025** post-deploy: ya no debe mostrar
|
||||
gasto negativo. Si lo hace, revisar si hay otros patrones (E que
|
||||
referencia el anticipo en lugar de la I/07 PPD).
|
||||
- **Decidir si aplicar a Adquisiciones G01**.
|
||||
- **Decidir si aplicar a IVA causado/acreditable** simétricamente.
|
||||
- **Considerar ampliar tolerancia a 1 mes después** si aparece un caso
|
||||
real con monto significativo.
|
||||
|
||||
---
|
||||
|
||||
## 7. Pestaña "Activos Fijos" en /impuestos
|
||||
|
||||
Vista informativa nueva para llevar seguimiento de la deducción mensual
|
||||
proporcional de activos fijos. **No altera dashboard ni ISR** — el SAT
|
||||
trata estos CFDIs como gasto del periodo, así que el sistema los sigue
|
||||
contando igual. Esta vista permite al contador planear la deducción
|
||||
manual en su declaración anual.
|
||||
|
||||
### Decisión clave del scope (con el user)
|
||||
|
||||
Inicialmente se evaluó excluir activos fijos del bucket de gastos y
|
||||
del cálculo de ISR. Se descartó porque eso desalineaba el sistema con
|
||||
el comportamiento del SAT (que sí considera el CFDI como gasto del
|
||||
periodo) y generaría confusión "el sistema no funciona". Decisión:
|
||||
sistema se mantiene como está, vista nueva sirve solo para
|
||||
**seguimiento informativo** del MOI.
|
||||
|
||||
### Modelo de cálculo
|
||||
|
||||
```
|
||||
MOI = total_mxn − iva_traslado_mxn − ieps_traslado_mxn − impuestos_locales_trasladado_mxn
|
||||
porcentajeMensual = porcentajeAnual / 12
|
||||
mesesTranscurridos = (year(periodo) − year(adq)) × 12 + (month(periodo) − month(adq)) + 1
|
||||
|
||||
acumuladoHastaMes = MIN(MOI, MOI × pctMensual × mesesTranscurridos)
|
||||
acumuladoHastaMesPrev = MIN(MOI, MOI × pctMensual × (mesesTranscurridos − 1))
|
||||
acreditableEsteMes = acumHasta − acumPrev
|
||||
saldoPendiente = MOI − acumHasta
|
||||
```
|
||||
|
||||
Si el activo se da de baja: `mesesAplicables = MIN(mesesTranscurridos,
|
||||
mesesEntreAdqYBaja)`. A partir del mes posterior a la baja,
|
||||
`acreditableEsteMes = 0`.
|
||||
|
||||
Dividir `% / 12` evita el problema del primer año (mes parcial) y
|
||||
permite seguimiento natural por periodo.
|
||||
|
||||
### Tabla de % LISR Art. 34
|
||||
|
||||
| Clave | Concepto | % anual |
|
||||
|---|---|---:|
|
||||
| I01 | Construcciones | 5% |
|
||||
| I02 | Mobiliario y equipo de oficina | 10% |
|
||||
| I03 | Equipo de transporte | 25% |
|
||||
| I04 | Equipo de cómputo y accesorios | 30% |
|
||||
| I05 | Dados, troqueles, moldes, matrices | 35% |
|
||||
| I06 | Comunicaciones telefónicas | 10% |
|
||||
| I07 | Comunicaciones satelitales | 8% |
|
||||
| I08 | Otra maquinaria y equipo | 10% |
|
||||
|
||||
### Filtros (qué CFDIs entran a esta vista)
|
||||
|
||||
- `tipo_comprobante = 'I'` y `status NOT IN ('Cancelado','0')`
|
||||
- `uso_cfdi ∈ {I01..I08}`
|
||||
- Receptor = contribuyente (`esReceptor`)
|
||||
- `regimen_fiscal_receptor ∈ {601, 606, 611, 612, 625, 626}`
|
||||
- **Para 626**: solo si `rfcLength === 12` (PM). RESICO PF (RFC 13)
|
||||
paga tasa plana sin restar deducciones.
|
||||
|
||||
### Estados
|
||||
|
||||
- `activo`: aún acreditable, no dado de baja, saldo > 0.
|
||||
- `agotado`: saldo = 0 (MOI ya se dedujo completo según meses
|
||||
transcurridos).
|
||||
- `baja_venta` / `baja_desecho` / `baja_otro`: el contador lo dio de
|
||||
baja con motivo correspondiente.
|
||||
|
||||
### Schema (migración 037)
|
||||
|
||||
```sql
|
||||
CREATE TABLE activos_fijos_baja (
|
||||
id serial PRIMARY KEY,
|
||||
cfdi_id int NOT NULL REFERENCES cfdis(id) ON DELETE CASCADE,
|
||||
fecha_baja date NOT NULL,
|
||||
motivo varchar(20) NOT NULL CHECK (motivo IN ('venta','desecho','otro')),
|
||||
comentario text,
|
||||
dado_de_baja_por uuid NOT NULL,
|
||||
created_at timestamptz DEFAULT now(),
|
||||
UNIQUE (cfdi_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Endpoints
|
||||
|
||||
```
|
||||
GET /api/impuestos/activos-fijos?año=YYYY&mes=MM&contribuyenteId=...&estado=...
|
||||
POST /api/impuestos/activos-fijos/:cfdiId/baja
|
||||
body: { fechaBaja, motivo: 'venta'|'desecho'|'otro', comentario? }
|
||||
DELETE /api/impuestos/activos-fijos/:cfdiId/baja
|
||||
```
|
||||
|
||||
### Archivos
|
||||
|
||||
- **Migración 037**: `037_activos_fijos_baja.sql`.
|
||||
- `apps/api/src/services/activos-fijos.service.ts`: cálculo + manejo
|
||||
de baja. Usa `resolveContribuyenteContext` para obtener `rfcLength`
|
||||
y filtrar 626 PM.
|
||||
- `apps/api/src/controllers/activos-fijos.controller.ts`: 3 handlers
|
||||
con Zod.
|
||||
- `apps/api/src/routes/impuestos.routes.ts`: 3 rutas montadas en
|
||||
`/api/impuestos/activos-fijos`.
|
||||
- `apps/web/components/impuestos/activos-fijos-tab.tsx`: componente
|
||||
con disclaimer (recordatorio de que es informativa), 4 KPIs (MOI,
|
||||
acumulado previo, este mes, saldo pendiente), filtro de estado,
|
||||
tabla con badge + acción de baja/reversa, modal de baja con motivo +
|
||||
fecha + comentario.
|
||||
- `apps/web/app/(dashboard)/impuestos/page.tsx`: botón nuevo
|
||||
"Activos Fijos" en el switch de tabs + render condicional.
|
||||
|
||||
### UX claves
|
||||
|
||||
- **Disclaimer ámbar** al inicio de la pestaña recordando que el
|
||||
sistema considera los CFDIs como gasto del periodo (igual que SAT)
|
||||
y esta vista es solo seguimiento, no afecta cálculos automáticos.
|
||||
- **Estados visuales** con badge de color (verde/gris/ámbar/rojo).
|
||||
- **Filtro de estado** (todos/activos/agotados/baja).
|
||||
- **Acción reversible**: dar de baja siempre se puede revertir
|
||||
(DELETE en `/baja`) — la fila vuelve a calcular meses normalmente.
|
||||
|
||||
### NO se tocó
|
||||
|
||||
- `calcularEgresosPorRegimen`, `calcularAdquisicionesMercancias`,
|
||||
`calcularResumenIsr`, `getIsrMensual`: intactos.
|
||||
- `metricas_mensuales` cache: no requiere recompute.
|
||||
- IVA causado/acreditable: sigue incluyendo estos CFDIs como antes.
|
||||
|
||||
### Filtro de conceptos por contribuyente (migración 038)
|
||||
|
||||
I06 (Comunicaciones telefónicas) y I07 (Comunicaciones satelitales)
|
||||
suelen usarse para **gastos regulares** (factura de teléfono, internet
|
||||
satelital) que no son adquisiciones de activos fijos. Para no ensuciar
|
||||
la vista, el contador puede excluir conceptos por contribuyente.
|
||||
|
||||
**Migración 038**:
|
||||
```sql
|
||||
ALTER TABLE contribuyentes
|
||||
ADD COLUMN activos_fijos_usos_excluidos jsonb DEFAULT '[]'::jsonb;
|
||||
```
|
||||
|
||||
**Endpoints**:
|
||||
```
|
||||
PUT /api/impuestos/activos-fijos/usos-excluidos
|
||||
body: { contribuyenteId, usos: ['I06','I07'] }
|
||||
```
|
||||
|
||||
El response del `GET /activos-fijos` incluye `usosExcluidos` (lista
|
||||
actual) para que el UI muestre badge "N excluidos".
|
||||
|
||||
**UI**: botón "Conceptos" en la barra de filtros abre modal con 8
|
||||
checkboxes (uno por uso I01-I08). Por default todos están marcados
|
||||
(considerados). Desmarcar = excluir. Persiste en BD.
|
||||
|
||||
### Pendientes derivados
|
||||
|
||||
- Auto-detectar bajas que vienen de CFDIs tipo egreso emitidos por el
|
||||
contribuyente que cancelan parcialmente un activo (ej. venta de
|
||||
equipo). Hoy es manual.
|
||||
- Vista anual con resumen por concepto y depreciación de cierre.
|
||||
- Conectar con declaraciones anuales: cuando el contador suba la
|
||||
declaración anual, mostrar checkbox para "este activo lo apliqué
|
||||
como deducción este ejercicio" para llevar trazabilidad.
|
||||
- Considerar nuevos usos CFDI introducidos por SAT en el futuro
|
||||
(mantener mapa centralizado).
|
||||
- Permitir excluir CFDIs específicos (no solo conceptos completos)
|
||||
para casos mixtos (ej. el cliente compra un teléfono celular
|
||||
ocasional que SÍ es activo, pero la factura mensual del servicio
|
||||
telefónico también es I06 y NO es activo).
|
||||
|
||||
---
|
||||
|
||||
## 8. Extensión IVA — compensación I PPD/07 ↔ E (turno 2026-04-26)
|
||||
|
||||
### Asimetría que motivó el cambio
|
||||
|
||||
El flujo del SAT con anticipo causa el IVA en tres puntos:
|
||||
- **Anticipo I PUE** — IVA causado/acreditado en su mes (PUE = se causa al emitir).
|
||||
- **Aplicación I/07** — la factura final que aplica el anticipo. Si es **PUE** aporta su IVA completo; si es **PPD** aporta 0 hasta que llegue el P.
|
||||
- **E que cancela** — NC formal o cancelación de operación.
|
||||
|
||||
En el caso **PUE** (aplicación I PUE/07), la cadena cierra algebraicamente
|
||||
gracias al filtro `bucketCausadoNeg/Acreditable` que excluye `tipoRelación='07'`
|
||||
y al SUM_REL_TRAS que compensa la I PUE/07 contra el anticipo. Sin
|
||||
necesidad de tocar nada.
|
||||
|
||||
En el caso **PPD**, la I PPD/07 no aporta nada en su mes (espera al P).
|
||||
Si en el **mismo mes** existe una E con tipoRelación **≠ 07** que la
|
||||
referencia, la E entra al `bucketAcreditableNeg` (o `bucketCausadoNeg`)
|
||||
y resta IVA — pero la I PPD/07 nunca aportó nada que la E pudiera
|
||||
neutralizar. Resultado: se "pierde" el IVA equivalente a la E.
|
||||
|
||||
### Implementación (`apps/api/src/services/impuestos.service.ts`)
|
||||
|
||||
Nuevos predicados/helpers:
|
||||
|
||||
- `IS_I_PPD_07` — gemelo de `IS_I_PUE_07` para metodo_pago='PPD'.
|
||||
- `SUM_E_REFERENCING_TRAS(esLadoE)` / `SUM_E_REFERENCING_RET(esLadoE)` —
|
||||
subqueries que suman el IVA de las E's que referencian la I PPD/07
|
||||
actual, filtrando por **mismo lado** y **mismo mes/año**.
|
||||
No filtran por `tipoRelación`: en PPD cualquier E que apunte a la
|
||||
I PPD/07 cuenta (incluyendo las 07, fiscalmente correctas).
|
||||
- `HAS_E_REFERENCING_MISMO_MES(esLadoE)` — EXISTS para incluir las
|
||||
I PPD/07 en `bucketCausadoAny`/`bucketAcreditableAny`. Sin filtro
|
||||
tipoRelación (consistente con `SUM_E_REFERENCING_*`).
|
||||
- `E_REFERENCIA_I_PPD_07_MISMO_MES(esLadoIAlias)` — EXISTS desde la
|
||||
fila E que verifica si esta E referencia una I PPD/07 del mismo
|
||||
lado/mes. Permite distinguir dos clases de E/07:
|
||||
- E/07 → anticipo I PUE puro (triángulo PUE clásico): EXISTS = false
|
||||
→ la E/07 queda excluida del NEG (statu quo, la lógica
|
||||
`SUM_REL_TRAS` de la I PUE/07 ya cierra el ciclo).
|
||||
- E/07 → I PPD/07 (cancelación de operación PPD): EXISTS = true
|
||||
→ la E/07 entra al NEG y resta IVA. La I PPD/07 hereda el mismo
|
||||
IVA via `SUM_E_REFERENCING_*`, neteando dentro del mes.
|
||||
|
||||
`bucketCausadoNeg` y `bucketAcreditableNeg` extendidos con el
|
||||
disyuntivo `OR E_REFERENCIA_I_PPD_07_MISMO_MES(...)` para que las
|
||||
E/07 que apuntan a I PPD/07 no queden filtradas. Los aliases `e` y
|
||||
`i` se derivan de `ctx.esEmisor`/`ctx.esReceptor` con el rewrite
|
||||
`replace(/\brfc_(emisor|receptor)\b/g, 'e.rfc_$1' | 'i.rfc_$1')`.
|
||||
|
||||
Rama nueva en los 4 signed exprs (`signedCausadoTras/Ret`,
|
||||
`signedAcreditableTras/Ret`):
|
||||
|
||||
```
|
||||
WHEN ${esLado} AND ${IS_I_PPD_07} THEN ${SUM_E_REFERENCING_*(esLadoE)}
|
||||
```
|
||||
|
||||
### Por qué la versión inicial filtraba `<> '07'` (descartado)
|
||||
|
||||
La primera implementación filtraba `tipoRelación <> '07'` en
|
||||
`SUM_E_REFERENCING_*` y `HAS_E_REFERENCING_MISMO_MES`, asumiendo que
|
||||
las E/07 estaban universalmente excluidas del NEG y que heredarlas
|
||||
sobre-acreditaría. Eso era cierto solo para el triángulo PUE puro,
|
||||
pero **ignoraba el caso fiscalmente correcto**: una E/07 que cancela
|
||||
una I PPD/07 sí debe restar IVA, porque la I PPD nunca aportó nada
|
||||
en su mes.
|
||||
|
||||
La corrección es discriminar **a qué apunta la E**, no qué tipoRelación
|
||||
tiene. Si apunta a una I PPD/07 → afecta IVA simétricamente (E resta
|
||||
en NEG, I PPD hereda en POS, netean a 0). Si apunta a un anticipo
|
||||
I PUE puro → queda excluida (statu quo).
|
||||
|
||||
### Validación con caso real Husberto 2025-08
|
||||
|
||||
Receptor TOAH680201RA2 con 4 CFDIs en `cfdis_relacionados` enredados:
|
||||
- Anticipo `729109fc` I PUE: $148K, IVA $20,413 → +$20,413 acreditable (POS, en su mes)
|
||||
- Aplicación `5c874749` I PPD/07: $454K, IVA $62,621 → hereda IVA total de las E del mismo mes
|
||||
- NC `7163da3b` E PUE/07: $148K, IVA $20,413 → ahora entra al NEG (apunta a I PPD/07)
|
||||
- NC `7aac715b` E PUE/01: $10K, IVA $1,379 → entra al NEG (tipoRelación ≠ 07)
|
||||
|
||||
| Concepto | Aporte agosto 2025 |
|
||||
|---|---:|
|
||||
| Anticipo I PUE (POS) | + $20,413.79 |
|
||||
| I PPD/07 hereda E/07 + E/01 (rama nueva) | + $21,793.10 |
|
||||
| E/07 (NEG, ahora incluida porque apunta a I PPD/07) | − $20,413.79 |
|
||||
| E/01 (NEG, ya estaba) | − $1,379.31 |
|
||||
| **Total acreditable** | **$20,413.79** |
|
||||
|
||||
| Estado | Acreditable agosto 2025 |
|
||||
|---|---:|
|
||||
| Antes del cambio | $20,413.79 + 0 + 0 − $1,379.31 = **$19,033.69** |
|
||||
| Después (versión inicial con filtro `<> '07'`) | $20,413.79 + $1,379.31 + 0 − $1,379.31 = **$20,413.79** |
|
||||
| Después (versión refinada sin filtro) | Ver tabla ↑ = **$20,413.79** |
|
||||
|
||||
**Delta total vs antes: +$1,379.31 acreditable recuperado.** Las dos
|
||||
versiones (con/sin filtro) dan el mismo resultado en el caso Husberto
|
||||
porque la E/01 y la E/07 cubren montos distintos. La versión refinada
|
||||
es necesaria para casos donde **solo existe la E/07** (lo correcto
|
||||
fiscalmente): sin la condición nueva en `bucketAcreditableNeg`, la
|
||||
E/07 quedaría excluida y la I PPD nunca sería incluida en el bucket
|
||||
Any → la compensación no ocurriría.
|
||||
|
||||
### Cache `metricas_mensuales`
|
||||
|
||||
`computeMetricaMensual` en `metricas-compute.service.ts` llama a
|
||||
`getResumenIva` que ya usa los signed exprs nuevos — futuros recomputes
|
||||
escriben los valores correctos. Periodos cacheados con la lógica vieja
|
||||
quedan stale hasta invalidarse. Pendiente: barrido de invalidación por
|
||||
periodo donde existan I PPD/07 + E/(≠07) referenciándolas en mismo mes.
|
||||
|
||||
### Por qué no se aplicó al caso PUE
|
||||
|
||||
El caso anticipo I PUE + I PUE/07 + E/07 ya cierra con la lógica
|
||||
existente (compensación SUM_REL_TRAS en I PUE/07, exclusión de E/07
|
||||
del NEG). Algebraicamente equivalente al flujo "natural" donde la E/07
|
||||
restaría — la diferencia es que el código actual es **robusto al caso
|
||||
"no se emite E/07"** (común en aplicaciones íntegras), donde el flujo
|
||||
natural sobrecausaría. Cambiar PUE rompería ese caso típico para
|
||||
ganar nada en el atípico.
|
||||
Reference in New Issue
Block a user