Files
shopify_run_run/tools/cargar_items_faltantes.py
T
georgeandClaude Sonnet 5.5 901dd8763d 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>
2026-09-30 19:22:40 -05:00

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)