Pipeline de integración de órdenes de Shopify a SAP Service Layer:
Invoice de reserva, pago con tarjeta o transferencia por transacción,
solicitud de traslado, traslado de stock y entrega.
- Invoice igual a lo cobrado; fulfillments limitados a lo facturado
- Estados terminales integrated, timeout, ignored y cancelled
- Idempotencia contra SAP: adopta documentos existentes en vez de duplicar
- Reintentos de red en el cliente de Shopify
- Secretos fuera de los JSON de tienda (${VARIABLE} resuelta desde .env)
- Herramientas de consulta, integración puntual y carga de pruebas
- Documentación en docs/ (flujo, casos especiales, mapeo, configuración)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
109 lines
3.8 KiB
Python
109 lines
3.8 KiB
Python
"""
|
|
Utilidad puntual (no forma parte del pipeline recurrente): recorre las
|
|
órdenes ya descargadas en SQLite para una tienda, trae el detalle completo
|
|
de cada una desde Shopify, extrae SKU + nombre de line_items, deduplica por
|
|
SKU, y crea en SAP los Items faltantes (grupo 108, artículo de venta +
|
|
inventario).
|
|
|
|
Uso:
|
|
.venv/bin/python tools/cargar_items_faltantes.py config/stores/snobbycl.json
|
|
.venv/bin/python tools/cargar_items_faltantes.py config/stores/snobbycl.json --apply
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from core.config import cargar_config_tienda
|
|
from core.db import db_session
|
|
from core.errors import SAPServiceLayerError
|
|
from core.sap import SAPServiceLayerClient
|
|
from core.shopify_client import ShopifyClient
|
|
from domain.items import item_existe
|
|
|
|
ITEMS_GROUP_CODE = 108
|
|
|
|
|
|
def obtener_shopify_order_ids(store_code: str) -> List[int]:
|
|
with db_session() as conn:
|
|
rows = conn.execute(
|
|
"SELECT shopify_order_id FROM orders WHERE store_code = ? ORDER BY shopify_order_id",
|
|
(store_code,),
|
|
).fetchall()
|
|
return [r["shopify_order_id"] for r in rows]
|
|
|
|
|
|
def extraer_skus(shopify: ShopifyClient, order_ids: List[int]) -> Dict[str, str]:
|
|
"""Devuelve {sku: nombre} deduplicado, recorriendo el detalle de cada orden."""
|
|
skus: Dict[str, str] = {}
|
|
for idx, order_id in enumerate(order_ids, start=1):
|
|
detalle = shopify.obtener_orden(order_id)
|
|
for item in detalle.get("line_items", []):
|
|
sku = (item.get("sku") or "").strip()
|
|
nombre = item.get("name") or item.get("title") or sku
|
|
if sku and sku not in skus:
|
|
skus[sku] = nombre
|
|
print(f" [{idx}/{len(order_ids)}] orden {order_id}: {len(detalle.get('line_items', []))} line_items")
|
|
return skus
|
|
|
|
|
|
def crear_item(sap: SAPServiceLayerClient, sku: str, nombre: str) -> None:
|
|
payload = {
|
|
"ItemCode": sku,
|
|
"ItemName": nombre[:100],
|
|
"ItemsGroupCode": ITEMS_GROUP_CODE,
|
|
"SalesItem": "tYES",
|
|
"InventoryItem": "tYES",
|
|
"PurchaseItem": "tNO",
|
|
}
|
|
sap.forward("POST", "Items", json_data=payload)
|
|
|
|
|
|
def main(store_config_path: str, dry_run: bool) -> None:
|
|
store_config = cargar_config_tienda(store_config_path)
|
|
store_code = store_config["store_code"]
|
|
|
|
shopify = ShopifyClient.from_store_config(store_config["shopify"])
|
|
sap = SAPServiceLayerClient.from_store_config(store_config["sap_connection"])
|
|
|
|
order_ids = obtener_shopify_order_ids(store_code)
|
|
print(f"Órdenes a revisar: {len(order_ids)}")
|
|
|
|
skus = extraer_skus(shopify, order_ids)
|
|
print(f"\nSKUs únicos encontrados: {len(skus)}\n")
|
|
|
|
existentes, creados, errores = 0, 0, 0
|
|
for sku, nombre in skus.items():
|
|
if item_existe(sap, sku):
|
|
existentes += 1
|
|
continue
|
|
|
|
if dry_run:
|
|
print(f"[DRY RUN] Crearía: {sku!r} - {nombre!r}")
|
|
continue
|
|
|
|
try:
|
|
crear_item(sap, sku, nombre)
|
|
creados += 1
|
|
print(f"Creado: {sku} - {nombre}")
|
|
except SAPServiceLayerError as exc:
|
|
errores += 1
|
|
print(f"ERROR creando {sku}: {exc.detail}")
|
|
|
|
faltantes = len(skus) - existentes
|
|
print(f"\nResumen: ya existían={existentes} | faltantes detectados={faltantes} | creados={creados} | errores={errores}")
|
|
if dry_run:
|
|
print("(dry-run: no se escribió nada en SAP, corré con --apply para crear los items)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("store_config", help="Ruta al JSON de configuración de la tienda")
|
|
parser.add_argument("--apply", action="store_true", help="Ejecuta el POST real (por defecto es dry-run)")
|
|
args = parser.parse_args()
|
|
main(args.store_config, dry_run=not args.apply)
|