Files
plantillas-proyectos/backend/tests/unit/invoices/test_validarseries_switch.py

230 lines
9.9 KiB
Python

"""
Tests para el switch maestro 'validarseries' y la validación 'cantvscantseries'.
Comportamiento esperado del código:
- La comprobación SERIES_VACIAS (sin series cuando has_serial=True) se emite
ANTES de leer el parámetro 'validarseries'; es una validación incondicional.
- El switch maestro 'validarseries' sólo controla la validación de MISMATCH
(cantidad de series vs cantidad de la partida), que es el check 'cantvscantseries'.
Casos cubiertos:
Importaciones (review_series):
1. validarseries=0 + series existen + mismatch → SIN error SERIES_VS_CANT (switch off)
2. validarseries=1 + cantvscantseries=0 + mismatch → SIN error SERIES_VS_CANT
3. validarseries=1 + cantvscantseries=1 + cantidad correcta → sin error
4. validarseries=1 + cantvscantseries=1 + mismatch → error SERIES_VS_CANT
5. has_serial=True + sin series → error SERIES_VACIAS (incondicional)
6. has_serial=False → sin validación (return inmediato)
Exportaciones (review_qty_series):
7. validarseries=0 + series existen + mismatch → SIN error SERIES_VS_CANT
8. validarseries=1 + cantvscantseries=1 + mismatch → error SERIES_VS_CANT
9. validarseries=1 + cantvscantseries=1 + match → sin error
10. has_serial=False → sin validación
"""
from decimal import Decimal
from types import SimpleNamespace
from unittest.mock import patch
from core.exceptions import ErrorCollector
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _mk_line(qty: Decimal, has_serial: bool = True, line_id: int = 9999):
return SimpleNamespace(
id=line_id,
line_number=1,
description=SimpleNamespace(has_serial=has_serial),
quantity=SimpleNamespace(quantity=qty),
unit_of_measure="PZA",
unit_of_measure_info=SimpleNamespace(code="PZA"),
)
def _mk_invoice_expo(is_regime_change: bool = False):
return SimpleNamespace(
compliance_mx=SimpleNamespace(is_regime_change=is_regime_change)
)
def _settings(*, validarseries: int = 0, cantvscantseries: int = 0) -> dict:
return {
"ssisgen": {
"validarseries": validarseries,
"cantvscantseries": cantvscantseries,
},
"qsisgen": {},
}
def _db_with_series_count(count: int):
"""Mock de DB que devuelve `count` al llamar .query(...).filter(...).count()."""
mock_query = SimpleNamespace(
filter=lambda *a, **kw: SimpleNamespace(count=lambda: count)
)
return SimpleNamespace(
query=lambda *a: mock_query,
get=lambda *a, **kw: SimpleNamespace(rfc="TST010101AAA"),
)
# ---------------------------------------------------------------------------
# Importaciones: review_series
# ---------------------------------------------------------------------------
class TestReviewSeriesImports:
def _call(self, db, line, settings: dict) -> ErrorCollector:
from api.v1.modules.a76.invoices.imports.process.sub_process.review_series import review_series
errors = ErrorCollector()
with patch(
"api.v1.modules.a76.invoices.imports.process.sub_process"
".review_series.AppSettingsService.get_resolved_settings",
return_value=settings,
):
review_series(db, line, "TST010101AAA", "1", "1", errors)
return errors
# -- Switch maestro apagado --------------------------------------------------
def test_switch_off_no_emite_series_vs_cant_aunque_haya_mismatch(self):
"""
validarseries=0: aunque hay 3 series para una qty=5, no se emite SERIES_VS_CANT.
SERIES_VACIAS no aplica porque sí existen series (count=3).
"""
db = _db_with_series_count(3)
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, line, _settings(validarseries=0))
codes = [e["code"] for e in errors.get_errors()]
assert "SERIES_VS_CANT" not in codes
assert not errors.has_errors()
# -- SERIES_VACIAS es incondicional (antes del switch) ----------------------
def test_sin_series_emite_series_vacias_independiente_de_validarseries(self):
"""
Cuando has_serial=True pero no hay ninguna serie registrada, se emite
SERIES_VACIAS siempre, incluso con validarseries=0.
(Esta validación ocurre antes de leer el parámetro.)
"""
db = _db_with_series_count(0)
line = _mk_line(qty=Decimal("5"), has_serial=True)
# Incluso con switch apagado, SERIES_VACIAS se emite
errors = self._call(db, line, _settings(validarseries=0))
codes = [e["code"] for e in errors.get_errors()]
assert "SERIES_VACIAS" in codes
# -- cantvscantseries=0 (switch on, pero sub-flag off) ----------------------
def test_cantvscantseries_off_no_valida_mismatch(self):
"""
validarseries=1 + cantvscantseries=0: aunque series(3) != qty(5), sin error.
"""
db = _db_with_series_count(3)
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, line, _settings(validarseries=1, cantvscantseries=0))
codes = [e["code"] for e in errors.get_errors()]
assert "SERIES_VS_CANT" not in codes
# -- Validación activa y sin mismatch ---------------------------------------
def test_validacion_activa_cantidad_correcta_sin_error(self):
"""validarseries=1 + cantvscantseries=1 + series==qty → sin error."""
db = _db_with_series_count(5)
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, line, _settings(validarseries=1, cantvscantseries=1))
assert not errors.has_errors()
# -- Validación activa con mismatch -----------------------------------------
def test_validacion_activa_mismatch_emite_error(self):
"""validarseries=1 + cantvscantseries=1 + series(3) != qty(5) → SERIES_VS_CANT."""
db = _db_with_series_count(3)
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, line, _settings(validarseries=1, cantvscantseries=1))
codes = [e["code"] for e in errors.get_errors()]
assert "SERIES_VS_CANT" in codes
# -- has_serial=False -------------------------------------------------------
def test_sin_lleva_serie_no_valida_nada(self):
"""Líneas sin has_serial hacen return inmediato; sin errores."""
db = _db_with_series_count(0)
line = _mk_line(qty=Decimal("5"), has_serial=False)
errors = self._call(db, line, _settings(validarseries=1, cantvscantseries=1))
assert not errors.has_errors()
# ---------------------------------------------------------------------------
# Exportaciones: review_qty_series
# ---------------------------------------------------------------------------
class TestReviewQtySeriesExports:
def _call(self, db, invoice, lines, settings: dict) -> ErrorCollector:
from api.v1.modules.a76.invoices.exports.process.sub_process.series.review_qty_series import (
review_qty_series,
)
errors = ErrorCollector()
with (
patch(
"api.v1.modules.a76.invoices.exports.process.sub_process"
".series.review_qty_series.AppSettingsService.get_resolved_settings",
return_value=settings,
),
patch(
"api.v1.modules.a76.invoices.exports.process.sub_process"
".series.review_qty_series.db.get",
return_value=SimpleNamespace(rfc="TST010101AAA"),
) if False else __import__("contextlib").nullcontext(), # get() ya en mock db
):
review_qty_series(db, invoice, lines, "1", "1", errors)
return errors
def test_switch_off_no_emite_series_vs_cant_aunque_haya_mismatch(self):
"""validarseries=0: series(2) != qty(5) pero sin error SERIES_VS_CANT."""
db = _db_with_series_count(2)
invoice = _mk_invoice_expo()
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, invoice, [line], _settings(validarseries=0))
codes = [e["code"] for e in errors.get_errors()]
assert "SERIES_VS_CANT" not in codes
assert not errors.has_errors()
def test_sin_series_emite_series_vacias_con_switch_on(self):
"""validarseries=1 + sin series registradas → SERIES_VACIAS."""
db = _db_with_series_count(0)
invoice = _mk_invoice_expo()
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, invoice, [line], _settings(validarseries=1, cantvscantseries=1))
codes = [e["code"] for e in errors.get_errors()]
assert "SERIES_VACIAS" in codes
def test_validacion_activa_mismatch_emite_error(self):
"""validarseries=1 + cantvscantseries=1 + series(2) != qty(5) → SERIES_VS_CANT."""
db = _db_with_series_count(2)
invoice = _mk_invoice_expo()
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, invoice, [line], _settings(validarseries=1, cantvscantseries=1))
codes = [e["code"] for e in errors.get_errors()]
assert "SERIES_VS_CANT" in codes
def test_validacion_activa_cantidad_correcta_sin_error(self):
"""validarseries=1 + cantvscantseries=1 + series==qty → sin error."""
db = _db_with_series_count(5)
invoice = _mk_invoice_expo()
line = _mk_line(qty=Decimal("5"), has_serial=True)
errors = self._call(db, invoice, [line], _settings(validarseries=1, cantvscantseries=1))
assert not errors.has_errors()
def test_sin_lleva_serie_no_valida_nada(self):
"""Líneas sin has_serial se ignoran."""
db = _db_with_series_count(0)
invoice = _mk_invoice_expo()
line = _mk_line(qty=Decimal("5"), has_serial=False)
errors = self._call(db, invoice, [line], _settings(validarseries=1, cantvscantseries=1))
assert not errors.has_errors()