#!/usr/bin/env python3 """ Script de corrección puntual para el pedido S00651. ==================================================== PROBLEMA: El pedido S00651 fue editado en Shopify (GRIS TALAVERA FMG HAT: 5 → 6). La orden de venta y la orden de entrega se actualizaron, pero el traslado interno ALMA/INT/00083 ya estaba validado con 5 unidades. SOLUCIÓN: Crear un traslado interno complementario de +1 unidad del ALMACÉN → OFICINA. USO: python3 fix_transfer_s00651.py --dry-run # Solo muestra lo que haría python3 fix_transfer_s00651.py --apply # Crea el traslado complementario """ import xmlrpc.client import sys import logging from datetime import datetime # ─── Configuración ──────────────────────────────────────────────────────── ODOO_URL = 'http://localhost:16001' ODOO_DB = 'hazard_new' ODOO_USER = 'admin' ODOO_PASS = 'admin' # ─── Datos del pedido afectado ──────────────────────────────────────────── SALE_ORDER_NAME = 'S00651' PRODUCT_SKU = 'HRS-HAT-FMG-U-GRY-OS' # GRIS TALAVERA FMG HAT QTY_FALTANTE = 1 # Diferencia: 6 - 5 = 1 # ─── Logging ────────────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', handlers=[logging.StreamHandler(sys.stdout)] ) logger = logging.getLogger(__name__) def connect(): common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common') uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASS, {}) if not uid: raise ConnectionError("No se pudo autenticar con Odoo") models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object') logger.info("Conectado a Odoo (uid=%s)", uid) return uid, models def main(): if len(sys.argv) < 2 or sys.argv[1] not in ('--dry-run', '--apply'): print(__doc__) sys.exit(1) mode = sys.argv[1] uid, models = connect() # 1. Buscar el pedido S00651 sale_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'sale.order', 'search', [[('name', '=', SALE_ORDER_NAME)]]) if not sale_ids: logger.error("No se encontró el pedido %s", SALE_ORDER_NAME) sys.exit(1) sale_id = sale_ids[0] sale_records = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'sale.order', 'read', [sale_id], {'fields': ['name', 'state', 'warehouse_id']}) sale = sale_records[0] logger.info("Pedido encontrado: %s (ID=%s, Estado=%s)", sale['name'], sale_id, sale['state']) # 2. Buscar el producto GRIS TALAVERA product_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'product.product', 'search', [[('default_code', '=', PRODUCT_SKU)]]) if not product_ids: logger.error("No se encontró el producto con SKU %s", PRODUCT_SKU) sys.exit(1) product_id = product_ids[0] product_records = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'product.product', 'read', [product_id], {'fields': ['name', 'display_name', 'default_code']}) product = product_records[0] logger.info("Producto encontrado: %s (ID=%s)", product['display_name'], product_id) # 3. Buscar el traslado interno original (ALMA/INT/00083) internal_move_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.move', 'search', [[ '|', ('picking_id.sale_id', '=', sale_id), ('picking_id.origin', '=', SALE_ORDER_NAME), ('picking_id.picking_type_id.code', '=', 'internal'), ('product_id', '=', product_id), ('state', '=', 'done'), ]]) if not internal_move_ids: logger.error("No se encontró movimiento interno completado para este producto en este pedido") sys.exit(1) move_records = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.move', 'read', [internal_move_ids[0]], {'fields': ['picking_id', 'product_uom_qty', 'product_uom', 'location_id', 'location_dest_id', 'warehouse_id']}) move_data = move_records[0] picking_id = move_data['picking_id'][0] picking_records = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.picking', 'read', [picking_id], {'fields': ['name', 'state', 'picking_type_id', 'location_id', 'location_dest_id']}) picking = picking_records[0] logger.info("Traslado original: %s (Estado=%s, Cantidad=%s)", picking['name'], picking['state'], move_data['product_uom_qty']) # 4. Verificar la orden de entrega delivery_move_ids = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.move', 'search', [[ '|', ('picking_id.sale_id', '=', sale_id), ('picking_id.origin', '=', SALE_ORDER_NAME), ('picking_id.picking_type_id.code', '=', 'outgoing'), ('product_id', '=', product_id), ('state', '!=', 'cancel'), ]]) if delivery_move_ids: del_move_records = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.move', 'read', [delivery_move_ids[0]], {'fields': ['product_uom_qty', 'picking_id']}) del_move = del_move_records[0] del_picking_records = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.picking', 'read', [del_move['picking_id'][0]], {'fields': ['name', 'state']}) del_picking = del_picking_records[0] logger.info("Orden de entrega: %s (Estado=%s, Demanda=%s)", del_picking['name'], del_picking['state'], del_move['product_uom_qty']) logger.info("") logger.info("═══════════════════════════════════════════════════") logger.info(" RESUMEN DE CORRECCIÓN") logger.info("═══════════════════════════════════════════════════") logger.info(" Pedido: %s", SALE_ORDER_NAME) logger.info(" Producto: %s (%s)", product['display_name'], PRODUCT_SKU) logger.info(" Traslado: %s (ya completado con %s unidades)", picking['name'], int(move_data['product_uom_qty'])) logger.info(" Faltante: +%s unidad(es)", QTY_FALTANTE) logger.info(" Acción: Crear traslado complementario ALMA → OFICINA") logger.info("═══════════════════════════════════════════════════") if mode == '--dry-run': logger.info("") logger.info(" 🔍 DRY RUN - No se modificó nada") logger.info(" Para aplicar ejecutar: python3 fix_transfer_s00651.py --apply") return # 5. Crear traslado complementario logger.info("") logger.info("Creando traslado complementario...") new_picking_id = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.picking', 'create', [{ 'picking_type_id': picking['picking_type_id'][0], 'location_id': picking['location_id'][0], 'location_dest_id': picking['location_dest_id'][0], 'origin': SALE_ORDER_NAME, 'sale_id': sale_id, 'move_type': 'direct', }]) new_move_id = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.move', 'create', [{ 'name': f"[Complemento S00651] {product['display_name']}", 'product_id': product_id, 'product_uom_qty': QTY_FALTANTE, 'product_uom': move_data['product_uom'][0], 'picking_id': new_picking_id, 'location_id': picking['location_id'][0], 'location_dest_id': picking['location_dest_id'][0], 'warehouse_id': move_data['warehouse_id'][0] if move_data['warehouse_id'] else False, }]) # Confirmar y reservar models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.picking', 'action_confirm', [[new_picking_id]]) models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.picking', 'action_assign', [[new_picking_id]]) # Leer el nombre del nuevo traslado new_picking = models.execute_kw(ODOO_DB, uid, ODOO_PASS, 'stock.picking', 'read', [new_picking_id], {'fields': ['name', 'state']}) logger.info("") logger.info("✅ Traslado complementario creado: %s (Estado: %s)", new_picking['name'], new_picking['state']) logger.info(" Producto: %s", product['display_name']) logger.info(" Cantidad: +%s unidad(es)", QTY_FALTANTE) logger.info(" Ruta: ALMA/Stock → OFFIC/Existencias") logger.info("") logger.info("⚡ SIGUIENTE PASO: Notifica a almacén para que valide el traslado %s", new_picking['name']) if __name__ == '__main__': main()