- sapb1_manager.buscar_factura_existente busca la factura (proveedor, tipo, serie, número) en facturas de compra no anuladas y en drafts; si existe no se crea otro draft y se responde "ya está registrada" sin pisar el registro de una aceptación anterior. - SapNoDisponible (subclase de ConnectionError) distingue una caída (timeout, sin conexión, HTTP 5xx) de un rechazo normal de SAP (HTTP 4xx). Una caída de Service Layer con HANA sin resultados ya no se lee como "guía no existe". - Con SAP o HANA caídos se avisa una sola vez al proveedor con la plantilla "En cola" y el correo se reintenta en los ciclos siguientes (hasta 24), sin repetir avisos; al agotarse se avisa al equipo. - La lógica por factura pasa a _procesar_factura; lo ya resuelto dentro de un correo no se repite al reintentarlo. - seguimiento: columna intentos (con migración), registrar_intento y el estado de notificación en_espera. - database_utilities.DB_NAME pasa a ser absoluto (junto al código). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
import observaciones as obs
|
|
|
|
|
|
def _item(numero, line_total=100.0, quantity=10.0, error=None):
|
|
item = {"guia": {"tipo": "09", "serie": "T001", "numero": numero}, "line_total": line_total, "quantity": quantity}
|
|
if error:
|
|
item["error"] = error
|
|
return item
|
|
|
|
|
|
def _resultado(ok, guias, sap_price=100.0, label="LineTotal (FL*) vs total_linea"):
|
|
return {"ok": ok, "guias_lt": guias, "sap_price": sap_price, "label": label}
|
|
|
|
|
|
FACTURA = {"lineas": [{"total_linea": 624.38, "descripcion": "Servicio A"}]}
|
|
|
|
|
|
def test_formato():
|
|
assert obs.moneda(1250) == "S/ 1,250.00"
|
|
assert obs.moneda(None) == "S/ 0.00"
|
|
assert obs.texto_observaciones(1) == "1 observación"
|
|
assert obs.texto_observaciones(3) == "3 observaciones"
|
|
|
|
|
|
def test_lineas_conformes_no_generan_observaciones():
|
|
assert obs.desde_validacion(FACTURA, [_resultado(True, [_item("1")])]) == []
|
|
|
|
|
|
def test_importe_distinto_muestra_ambos_importes():
|
|
(o,) = obs.desde_validacion(FACTURA, [_resultado(False, [_item("1"), _item("2")], sap_price=674.38)])
|
|
|
|
assert o.codigo == "precio_no_coincide"
|
|
assert dict(o.filas) == {"Guías": "T001-1, T001-2", "Importe en su factura": "S/ 624.38", "Importe esperado": "S/ 674.38"}
|
|
assert "Línea" not in dict(o.filas) # con una sola línea no hace falta numerarla
|
|
|
|
|
|
def test_con_varias_lineas_se_indica_cual():
|
|
factura = {"lineas": [{"total_linea": 10}, {"total_linea": 20}]}
|
|
resultados = [_resultado(True, [_item("1")]), _resultado(False, [_item("2")], sap_price=25.0)]
|
|
|
|
(o,) = obs.desde_validacion(factura, resultados)
|
|
|
|
assert dict(o.filas)["Línea"] == "2"
|
|
|
|
|
|
def test_guia_sin_datos_en_sap_es_guia_no_encontrada_y_no_importe():
|
|
guias = [_item("1", 0.0, 0.0), _item("2", 0.0, 0.0)]
|
|
observaciones = obs.desde_validacion(FACTURA, [_resultado(False, guias, 0.0, "Sin guías SAP")])
|
|
|
|
assert [o.codigo for o in observaciones] == ["guia_no_encontrada", "guia_no_encontrada"]
|
|
assert [o.titulo for o in observaciones] == ["No encontramos la guía T001-1", "No encontramos la guía T001-2"]
|
|
|
|
|
|
def test_una_guia_encontrada_y_otra_no_da_ambas_observaciones():
|
|
guias = [_item("1", 0.0, 0.0), _item("2", 40.0, 5.0)]
|
|
observaciones = obs.desde_validacion(FACTURA, [_resultado(False, guias, sap_price=40.0)])
|
|
|
|
assert [o.codigo for o in observaciones] == ["guia_no_encontrada", "precio_no_coincide"]
|
|
|
|
|
|
def test_una_guia_con_error_de_consulta_no_se_reporta_como_no_encontrada():
|
|
guias = [_item("1", 0.0, 0.0, error="timeout")]
|
|
assert obs.desde_validacion(FACTURA, [_resultado(False, guias, 0.0)]) == [
|
|
obs.precio_no_coincide(1, 624.38, 0.0, ["T001-1"], False)
|
|
]
|
|
|
|
|
|
def test_lineas_sin_guia_o_con_cantidad_cero():
|
|
factura = {"lineas": [{"descripcion": "Flete a Lima", "total_linea": 5}, {"total_linea": 7}]}
|
|
resultados = [_resultado(False, [], label="Sin guía de referencia"), _resultado(False, [], label="Cantidad cero, sin validar")]
|
|
|
|
sin_guia, cero = obs.desde_validacion(factura, resultados)
|
|
|
|
assert sin_guia.codigo == "linea_sin_guia" and dict(sin_guia.filas) == {"Línea": "1", "Descripción": "Flete a Lima"}
|
|
assert cero.codigo == "cantidad_cero" and dict(cero.filas) == {"Línea": "2"}
|
|
|
|
|
|
def test_motivos_de_correo_sin_comprobante():
|
|
assert obs.sin_pdf_xml(2).filas == (("Archivos recibidos", "2"),)
|
|
assert obs.falta_xml(1).codigo == "falta_xml"
|
|
assert obs.sin_factura(["Guía de Remisión"]).filas == (("Documentos recibidos", "Guía de Remisión"),)
|
|
assert obs.sin_factura([]).filas == (("Documentos recibidos", "ninguno reconocible"),)
|
|
|
|
|
|
def test_todas_las_observaciones_tienen_titulo_y_como_corregir():
|
|
muestras = [
|
|
obs.precio_no_coincide(1, 1, 2, ["T001-1"], True), obs.guia_no_encontrada("T001-1"),
|
|
obs.linea_sin_guia(1, ""), obs.cantidad_cero(1), obs.factura_ya_registrada("F002-1"), obs.sin_pdf_xml(0), obs.falta_xml(1), obs.sin_factura([]),
|
|
]
|
|
for o in muestras:
|
|
assert o.titulo and o.como_corregir and o.codigo
|
|
assert len({o.codigo for o in muestras}) == len(muestras)
|
|
|
|
|
|
def test_a_dict_es_serializable():
|
|
d = obs.guia_no_encontrada("T001-9").a_dict()
|
|
assert d == {"codigo": "guia_no_encontrada", "titulo": "No encontramos la guía T001-9",
|
|
"filas": [{"etiqueta": "Guía", "valor": "T001-9"}], "como_corregir": d["como_corregir"]}
|
|
|
|
|
|
def test_factura_ya_registrada():
|
|
o = obs.factura_ya_registrada("F002-486")
|
|
assert o.codigo == "factura_ya_registrada" and o.filas == (("Comprobante", "F002-486"),)
|
|
assert "No necesita enviarla otra vez" in o.como_corregir
|
|
|