- Custom modules: hazard_shopify, hazard_inventory_report, hazard_website - Third-party modules: bi_sql_editor, query_deluxe, sql_request_abstract, l10n_mx_sat_sync_itadmin_ee - Docker setup: Odoo 16 + PostgreSQL 17 - Utility scripts: backup_hazard.sh, update_module.py - Agent configuration: .agents/AGENTS.md - Security: Enterprise modules, credentials, backups and production data excluded via .gitignore
195 lines
8.7 KiB
Python
195 lines
8.7 KiB
Python
#!/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()
|