Integración Shopify → SAP Business One (snobbycl y on-chile)
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>
This commit is contained in:
commit
901dd8763d
47 files changed
+4709
No files matched your search
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
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)
|
||||
Reference in new issue
Block a user