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>
123 lines
5.5 KiB
Python
123 lines
5.5 KiB
Python
"""
|
|
Integra órdenes puntuales de una tienda, de inicio a fin (Invoice -> Payment
|
|
-> ITR -> StockTransfer -> Delivery), sin pasar por el ciclo del scheduler.
|
|
|
|
Por defecto es DRY-RUN: valida cada orden y muestra el payload de la Invoice
|
|
y del Payment (este último armado sobre un total equivalente) sin escribir en
|
|
SAP ni en la base de datos. Con --apply inserta la orden en `orders` y corre
|
|
el pipeline real para ese id (crea documentos en SAP).
|
|
|
|
Uso:
|
|
python -m tools.integrar_ordenes <store_code> <shopify_order_id> [...] # dry-run
|
|
python -m tools.integrar_ordenes <store_code> <shopify_order_id> [...] --apply
|
|
"""
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from core.config import cargar_config_tienda
|
|
from core.db import get_order_sys_id, insert_order_header, db_session
|
|
from core.logger_manager import order_context
|
|
from core.sap import SAPServiceLayerClient
|
|
from core.shopify_client import ShopifyClient
|
|
from domain.business_partner import buscar_rut_en_order
|
|
from domain.inventory_transfer import resolver_location
|
|
from domain.invoice import construir_invoice
|
|
from domain.payment import PaymentConfigError, construir_incoming_payment
|
|
from domain.rut import formatear_rut
|
|
from pipeline.runner import ESTADOS_FINANCIEROS_FACTURABLES, analizar_y_procesar_orden
|
|
|
|
STORES_DIR = Path(__file__).resolve().parent.parent / "config" / "stores"
|
|
|
|
|
|
def _dry_run(cfg: dict, shopify: ShopifyClient, order_id: int) -> None:
|
|
detalle = shopify.obtener_orden(order_id)
|
|
print(f"{detalle.get('name')} | financial={detalle.get('financial_status')} fulfillment={detalle.get('fulfillment_status')}")
|
|
|
|
if detalle.get("cancelled_at"):
|
|
print(f"-> cancelled (anulada el {detalle['cancelled_at'][:10]}): no se crearía nada")
|
|
return
|
|
if detalle.get("financial_status") not in ESTADOS_FINANCIEROS_FACTURABLES or not detalle.get("fulfillments"):
|
|
print("-> not_eligible (no está pagada o no tiene fulfillments): no se crearía nada")
|
|
return
|
|
|
|
envio = float(((detalle.get("total_shipping_price_set") or {}).get("shop_money") or {}).get("amount", 0) or 0)
|
|
if envio > 0:
|
|
print(f"-> error: costo de envío ({envio}) no soportado")
|
|
return
|
|
|
|
rut = buscar_rut_en_order(detalle)
|
|
card_code = cfg["sap"]["generic_card_code"] if not rut else "(se resuelve en SAP por RUT)"
|
|
print(f"RUT: {rut or 'sin RUT en note_attributes'} -> CardCode: {card_code}")
|
|
|
|
for f in detalle["fulfillments"]:
|
|
loc = resolver_location(cfg, f["location_id"])
|
|
print(f"fulfillment {f['id']} location {f['location_id']} -> {loc}")
|
|
|
|
if rut:
|
|
print("(orden con RUT: la Invoice real depende del BusinessPartner en SAP; se muestra con CardCode genérico)")
|
|
transacciones = shopify.obtener_transacciones(order_id)
|
|
payload = construir_invoice(
|
|
detalle, cfg, cfg["sap"]["generic_card_code"], federal_tax_id=formatear_rut(rut) if rut else None, transactions=transacciones
|
|
)
|
|
print("--- Invoice ---")
|
|
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
|
|
total = sum(l["Quantity"] * l["PriceAfterVAT"] for l in payload["DocumentLines"])
|
|
invoice_sim = {"CardCode": payload["CardCode"], "CardName": payload.get("CardName", ""), "DocDate": payload["DocDate"]}
|
|
try:
|
|
pago = construir_incoming_payment(detalle, transacciones, cfg, invoice_sim, 0, total)
|
|
print("--- IncomingPayment (DocEntry de la Invoice = 0 en dry-run) ---")
|
|
print(json.dumps(pago, indent=2, ensure_ascii=False))
|
|
except PaymentConfigError as exc:
|
|
print(f"--- IncomingPayment: ERROR, no se crearía el pago: {exc}")
|
|
|
|
|
|
def _apply(cfg: dict, shopify: ShopifyClient, order_id: int) -> None:
|
|
store_code = cfg["store_code"]
|
|
orden = shopify.obtener_orden(order_id)
|
|
insert_order_header(
|
|
store_code,
|
|
{
|
|
"shopify_order_id": orden["id"],
|
|
"order_number": orden.get("name"),
|
|
"tags": orden.get("tags"),
|
|
"shopify_created_at": orden.get("created_at"),
|
|
"financial_status": orden.get("financial_status"),
|
|
"fulfillment_status": orden.get("fulfillment_status"),
|
|
},
|
|
)
|
|
sys_id = get_order_sys_id(store_code, order_id)
|
|
with db_session() as conn:
|
|
row = dict(conn.execute("SELECT * FROM orders WHERE sys_id = ?", (sys_id,)).fetchone())
|
|
with order_context(store_code, order_id):
|
|
ok = analizar_y_procesar_orden(cfg, row)
|
|
with db_session() as conn:
|
|
estado = conn.execute("SELECT status, last_error FROM orders WHERE sys_id = ?", (sys_id,)).fetchone()
|
|
docs = conn.execute(
|
|
"SELECT doc_type, status, sap_doc_num, error_message FROM order_documents WHERE order_sys_id = ? ORDER BY sequence",
|
|
(sys_id,),
|
|
).fetchall()
|
|
print(f"resultado={ok} status={estado['status']} last_error={estado['last_error']}")
|
|
for d in docs:
|
|
print(f" {d['doc_type']}: {d['status']} DocNum={d['sap_doc_num']} {d['error_message'] or ''}")
|
|
|
|
|
|
def main() -> None:
|
|
args = sys.argv[1:]
|
|
apply = "--apply" in args
|
|
args = [a for a in args if a != "--apply"]
|
|
store_code, ids = args[0], [int(x) for x in args[1:]]
|
|
|
|
cfg = cargar_config_tienda(STORES_DIR / f"{store_code}.json")
|
|
shopify = ShopifyClient.from_store_config(cfg["shopify"])
|
|
|
|
print(f"MODO: {'APPLY (escribe en SAP)' if apply else 'DRY-RUN (no escribe nada)'} | tienda {store_code} | {len(ids)} órdenes")
|
|
for order_id in ids:
|
|
print(f"\n===== Orden {order_id} =====")
|
|
(_apply if apply else (lambda c, s, o: _dry_run(c, s, o)))(cfg, shopify, order_id)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|