230 lines
9.9 KiB
Python
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()
|