import math from datetime import datetime, timedelta import pytest import draft_sap # ============================================================================ # calcular_fecha_vencimiento # ============================================================================ @pytest.mark.parametrize("condicion, esperado", [ (None, "2026-01-10"), ({"NumberOfAdditionalDays": 30, "NumberOfAdditionalMonths": 0}, "2026-02-09"), ({"NumberOfAdditionalDays": 0, "NumberOfAdditionalMonths": 2}, "2026-03-11"), # 2 meses = 60 días ({"NumberOfAdditionalDays": None, "NumberOfAdditionalMonths": None}, "2026-01-10"), ({"NumberOfAdditionalDays": 15, "NumberOfAdditionalMonths": 3}, "2026-01-25"), # los días priman ]) def test_calcular_fecha_vencimiento(condicion, esperado): assert draft_sap.calcular_fecha_vencimiento("2026-01-10", condicion) == esperado # ============================================================================ # validate_prices # ============================================================================ def _delivery_note(line_total=100.0, pav=1.5, qty=10, almacen="A-X"): return [{ "Series": "1", "DocNum": "9", "U_IDC_DESTINO": "Z1", "DocumentLines": [ {"ItemCode": "FL10007", "LineTotal": line_total, "PriceAfterVAT": pav, "Quantity": qty, "WarehouseCode": almacen, "Weight1": 5}, {"ItemCode": "OTRO", "LineTotal": 999, "PriceAfterVAT": 9, "Quantity": 9}, # no FL\d: se ignora ], }] def _guia(numero="1"): return {"tipo": "09", "serie": "T001", "numero": numero} @pytest.fixture def sap(monkeypatch): """SAP simulado: devuelve la misma DeliveryNote para toda guía.""" estado = {"dn": _delivery_note()} monkeypatch.setattr(draft_sap.sapb1_manager, "get_zonas", lambda: [{"Code": "Z1", "Name": "ZONA1"}]) monkeypatch.setattr(draft_sap.sapb1_manager, "get_delivery_notes", lambda **k: estado["dn"]) return estado def test_precios_coinciden_usa_valores_de_sap(sap): factura = {"lineas": [{"cantidad": 1, "total_linea": 100.0, "guias_remision": [_guia()]}]} ok, resultados = draft_sap.validate_prices(factura) assert ok is True item = resultados[0]["guias_lt"][0] assert resultados[0]["ok"] is True assert (item["draft_quantity"], item["draft_price"]) == (10, 1.5) # de SAP, no del XML assert item["zona"] == "ZONA1" assert item["warehouse_code"] == "A-X" assert item["weight1"] == 5.0 def test_precios_no_coinciden_usa_valores_del_xml(sap): factura = {"lineas": [{"cantidad": 4, "total_linea": 90.0, "guias_remision": [_guia()]}]} ok, resultados = draft_sap.validate_prices(factura) assert ok is False item = resultados[0]["guias_lt"][0] assert (item["draft_quantity"], item["draft_price"]) == (4, 22.5) # 90 / 4 def test_varias_guias_de_una_linea_se_suman(sap): sap["dn"] = _delivery_note(line_total=60.0) factura = {"lineas": [{"cantidad": 1, "total_linea": 120.0, "guias_remision": [_guia("1"), _guia("2")]}]} ok, resultados = draft_sap.validate_prices(factura) assert ok is True assert resultados[0]["sap_price"] == 120.0 assert len(resultados[0]["guias_lt"]) == 2 def test_tolerancia_de_un_centimo(sap): factura = {"lineas": [{"cantidad": 1, "total_linea": 100.005, "guias_remision": [_guia()]}]} assert draft_sap.validate_prices(factura)[0] is True factura["lineas"][0]["total_linea"] = 100.02 assert draft_sap.validate_prices(factura)[0] is False def test_cantidad_cero_no_se_valida(sap): factura = {"lineas": [{"cantidad": 0, "total_linea": 100.0, "guias_remision": [_guia()]}]} ok, resultados = draft_sap.validate_prices(factura) assert ok is False assert resultados[0]["label"] == "Cantidad cero, sin validar" assert resultados[0]["guias_lt"] == [] def test_linea_sin_guia_no_se_valida(sap): factura = {"lineas": [{"cantidad": 1, "total_linea": 100.0, "guias_remision": []}]} ok, resultados = draft_sap.validate_prices(factura) assert ok is False assert resultados[0]["label"] == "Sin guía de referencia" def test_guia_inexistente_en_sap_queda_sin_validar(sap): sap["dn"] = [] factura = {"lineas": [{"cantidad": 1, "total_linea": 100.0, "guias_remision": [_guia()]}]} ok, resultados = draft_sap.validate_prices(factura) assert ok is False assert resultados[0]["label"] == "Sin guías SAP" def test_error_consultando_sap_invalida_la_linea(monkeypatch, sap): def falla(**k): raise RuntimeError("sin conexión") monkeypatch.setattr(draft_sap.sapb1_manager, "get_delivery_notes", falla) factura = {"lineas": [{"cantidad": 1, "total_linea": 0.0, "guias_remision": [_guia()]}]} ok, resultados = draft_sap.validate_prices(factura) assert ok is False # aunque 0 == 0, el error obliga a ok=False assert resultados[0]["guias_lt"][0]["line_total"] == 0.0 # ============================================================================ # build_sap_draft # ============================================================================ def _factura(fecha_emision, **extra): base = { "serie": "F002", "numero": "486", "tipo": "01", "total_factura": 118.0, "fecha_emision": fecha_emision, "ruc_emisor": "20537929520", "razon_social_emisor": "TRANSPORTES SA", "tiene_detraccion": False, "ruta_red_pdf": "\\\\srv\\a.pdf", "ruta_red_xml": "\\\\srv\\a.xml", "lineas": [ {"descripcion": "Servicio A", "valor_referencial": 10.5}, {"descripcion": "Servicio B", "valor_referencial": 4.5}, ], } base.update(extra) return base def _resultados(draft_price=1.23456789, draft_quantity=3): item = {"guia": {"tipo": "09", "serie": "T001", "numero": "100"}, "draft_price": draft_price, "draft_quantity": draft_quantity, "warehouse_code": "A-X"} return [{"guias_lt": [item]}, {"guias_lt": []}] @pytest.fixture def condicion_pago(monkeypatch): monkeypatch.setattr(draft_sap.sapb1_manager, "obtener_condicion_pago", lambda card_code: {"NumberOfAdditionalDays": 30}) def test_draft_cabecera_y_linea(condicion_pago): hoy = datetime.now() draft = draft_sap.build_sap_draft(_factura(hoy.strftime("%Y-%m-%d")), _resultados()) assert draft["CardCode"] == "PL20537929520" assert draft["NumAtCard"] == "01F002-486" assert draft["DocDate"] == hoy.strftime("%Y-%m-%d") assert draft["DocDueDate"] == (hoy + timedelta(days=30)).strftime("%Y-%m-%d") assert draft["U_IDC_VALREF"] == 15.0 # suma del XML assert draft["U_IDC_NUMGR"] == "09T001-100" assert draft["U_VS_ANEXO_PDF"] == "\\\\srv\\a.pdf" assert len(draft["DocumentLines"]) == 1 # la línea sin guías se omite linea = draft["DocumentLines"][0] assert linea["Quantity"] == 3 assert linea["PriceAfterVAT"] == math.trunc(1.23456789 * 1_000_000) / 1_000_000 == 1.234567 assert linea["U_IDC_GRFLETE"] == "09T001-100" assert linea["WarehouseCode"] == "A-X" assert draft["DocumentInstallments"] == [ {"DueDate": hoy.strftime("%Y-%m-%d"), "Total": 118.0, "InstallmentId": 1, "U_VS_TIPOCUOTA": "N", "U_VS_TIPOSERVICIO": ""}, ] def test_item_code_segun_mes_de_emision(condicion_pago): hoy = datetime.now() mes_anterior = (hoy.replace(day=1) - timedelta(days=1)).strftime("%Y-%m-%d") actual = draft_sap.build_sap_draft(_factura(hoy.strftime("%Y-%m-%d")), _resultados()) anterior = draft_sap.build_sap_draft(_factura(mes_anterior), _resultados()) assert actual["DocumentLines"][0]["ItemCode"] == "C103002" assert anterior["DocumentLines"][0]["ItemCode"] == "C103002-A" def test_draft_con_detraccion_genera_dos_cuotas(condicion_pago): fecha = datetime.now().strftime("%Y-%m-%d") factura = _factura(fecha, tiene_detraccion=True, codigo_detraccion="004", porcentaje_detraccion=12, monto_detraccion=11.2) draft = draft_sap.build_sap_draft(factura, _resultados()) detraccion, neto = draft["DocumentInstallments"] assert (detraccion["Total"], detraccion["U_VS_TIPOCUOTA"], detraccion["U_VS_TIPOSERVICIO"]) == (12, "D", "00402") assert (neto["Total"], neto["U_VS_TIPOCUOTA"]) == (106.0, "N") # 118 - ceil(11.2) assert draft["U_VS_AFEDET"] == "Y" assert draft["U_VS_CODSER"] == "00402" assert draft["U_VS_PORDET"] == 12 def test_draft_sin_guias_validadas_queda_sin_lineas(condicion_pago): fecha = datetime.now().strftime("%Y-%m-%d") draft = draft_sap.build_sap_draft(_factura(fecha), [{"guias_lt": []}, {"guias_lt": []}]) assert draft["DocumentLines"] == [] assert draft["U_IDC_NUMGR"] == "" def test_draft_sin_condicion_de_pago_vence_el_dia_de_contabilizacion(monkeypatch): monkeypatch.setattr(draft_sap.sapb1_manager, "obtener_condicion_pago", lambda c: None) fecha = datetime.now().strftime("%Y-%m-%d") draft = draft_sap.build_sap_draft(_factura(fecha), _resultados()) assert draft["DocDueDate"] == draft["DocDate"] # ============================================================================ # obtener_valor_referencial_sap # ============================================================================ def test_valor_referencial_consulta_una_vez_por_guia(monkeypatch): llamadas = [] monkeypatch.setattr(draft_sap.hana_manager, "obtener_valor_referencial", lambda serie, numero: llamadas.append((serie, numero)) or 7.5) g = {"tipo": "09", "serie": "T001", "numero": "1"} factura = {"lineas": [ {"guias_remision": [g]}, {"guias_remision": [g, {"tipo": "09", "serie": "T001", "numero": "2"}]}, ]} resultado = draft_sap.obtener_valor_referencial_sap(factura) assert llamadas == [("T001", "1"), ("T001", "2")] assert [r["valor_sap"] for r in resultado] == [7.5, 7.5]