Backport de mejoras genéricas de backend (desde cliente/tagumedica) + limpieza de datos de cliente en main

Mejoras genéricas traídas:
- core/crud_router.py: el PATCH genérico reenvía cualquier header B1S-* recibido del caller en vez de decidirlo el backend.
- routers/catalogs.py: nuevo endpoint /api/Catalogs/UdfValidValues (valores válidos de cualquier UDF vía Service Layer).
- routers/batch.py: /api/batch/stock (stock de un lote por almacén) y /api/batch/lot-source-warehouse (resuelve contra HANA de qué almacén salió un lote, repartiendo por origen).
- routers/inventory_transfer_requests.py + core/inventory_transfer_requests.py: CRUD genérico de Solicitudes de Transferencia de Inventario.
- routers/series.py + core/series.py: series activas de un tipo de documento.
- routers/user_tables.py: catálogo Code/Name genérico de tablas de usuario (mapeo USER_TABLES vacío, cada cliente completa el suyo).
- routers/reports.py / templates/reports/stickers.html: fix de márgenes/alto de página del reporte de etiquetas.
- mini-backend/test/ -> mini-backend/tools/ (rename organizativo).

Limpieza de datos de Tagumedica que ya estaban en el commit base de main (anteriores a la branch cliente/tagumedica, no relacionados con el backport):
- tools/sap-tagumedica/sl.http: eliminado (tenía CompanyDB y password reales).
- tools/ApiGateway/Login.yml: CompanyDB/host/password reemplazados por placeholders.
- templates/reports/stickers.html: título hardcodeado "TAGUMEDICA" reemplazado por "EMPRESA".
- menu_permissions.json: reemplazado por un ejemplo demo (usuarios/módulos ficticios) en vez de emails reales.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5 committed 2026-09-28 14:18:42 -05:00
1 parent b2fb4e2d0a
commit fef21898f9
30 files changed
+462 -112

No files matched your search

+11 -1
View File
@@ -113,11 +113,21 @@ def make_crud_router(
@router.patch(f"/api/{resource}/{{key}}", status_code=204, name=f"update_{resource}")
async def update_resource(key: str, request: Request):
data = await request.json()
# Las cabeceras de Service Layer (B1S-*) las decide quien llama
# (el frontend), no el backend: cualquier header "B1S-*" que
# venga en el request se reenvía tal cual a SAP. Ej.:
# B1S-ReplaceCollectionsOnPatch para reemplazar una colección
# hija (StockTransferLines) en vez de mergearla por LineNum,
# necesario solo cuando ese PATCH agrega o quita líneas.
headers = dict(patch_headers or {})
headers.update(
{k: v for k, v in request.headers.items() if k.lower().startswith("b1s-")}
)
forward_to_sapb1(
method="PATCH",
endpoint_path=key_expr(key),
json_data=data,
extra_headers=patch_headers,
extra_headers=headers or None,
)
return router
@@ -0,0 +1,4 @@
# Objeto Service Layer para la Solicitud de Transferencia de Inventario
# (BoObjectTypes "1250000001"). Centralizado acá para que el router de
# routers/inventory_transfer_requests.py no repita el nombre del endpoint.
INVENTORY_TRANSFER_REQUESTS_RESOURCE = "InventoryTransferRequests"
+51
View File
@@ -0,0 +1,51 @@
import logging
from typing import List, Optional
from core.sap import forward_to_sapb1
logger = logging.getLogger(__name__)
def get_default_series(document_type: str) -> Optional[int]:
"""
Consulta a Service Layer (SeriesService_GetDefaultSeries) la serie activa
por defecto para un tipo de documento de SAP B1 (código BoObjectTypes,
ej. "1250000001" para Solicitud de Transferencia). Si la llamada falla o
la respuesta no trae el campo esperado, se registra en el log y se
devuelve None; quien llama decide cómo seguir sin una serie por defecto.
"""
try:
response = forward_to_sapb1(
method="POST",
endpoint_path="SeriesService_GetDefaultSeries",
json_data={"DocumentTypeParams": {"Document": document_type}},
)
except Exception as exc:
logger.warning(
f"No se pudo obtener la serie por defecto de SAP (documento {document_type}): {exc}"
)
return None
data = response.json()
series = data.get("Series")
if series is None:
logger.warning(
f"SeriesService_GetDefaultSeries no devolvió 'Series' (documento {document_type}): {data}"
)
return series
def get_active_series(document_type: str) -> List[dict]:
"""
Lista las series no bloqueadas de un tipo de documento de SAP B1, vía
SeriesService_GetDocumentSeries. Confirmado contra Service Layer real:
la respuesta viene en "value", cada item con "Series" (código) y
"Name"; "Locked" en "tYES"/"tNO".
"""
response = forward_to_sapb1(
method="POST",
endpoint_path="SeriesService_GetDocumentSeries",
json_data={"DocumentTypeParams": {"Document": document_type}},
)
series_list = response.json().get("value", [])
return [s for s in series_list if str(s.get("Locked", "")).lower() != "tyes"]