40-clients/ucepsa/edge-oee-demo/tools/odoo_margin_commercial_sync.py
2026-06-25 18:51:33 +02:00

529 lines
16 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import json
import os
import sys
import xmlrpc.client
from datetime import datetime, timezone
try:
import psycopg2
import psycopg2.extras
except Exception as exc:
print(f"ERROR: psycopg2 no disponible en esta imagen Python: {exc}", file=sys.stderr)
sys.exit(2)
TENANT = os.getenv("TENANT_OVERRIDE", "ucepsa")
SITE = os.getenv("SITE_OVERRIDE", "ucepsa_onpremise")
ODOO_INSTANCE = os.getenv("ODOO_INSTANCE_OVERRIDE", "odoo_ucepsa_prod")
ODOO_DB = os.getenv("ODOO_DB_OVERRIDE") or os.getenv("ODOO_DB")
ODOO_URL = (os.getenv("ODOO_URL") or "").rstrip("/")
ODOO_USER = os.getenv("ODOO_USER")
ODOO_PASSWORD = os.getenv("ODOO_PASSWORD")
PGHOST = os.getenv("POSTGRES_HOST") or os.getenv("PGHOST") or "mv_ucepsa_postgres_hot"
PGPORT = int(os.getenv("POSTGRES_PORT") or os.getenv("PGPORT") or "5432")
PGDATABASE = os.getenv("POSTGRES_DB") or os.getenv("PGDATABASE")
PGUSER = os.getenv("POSTGRES_USER") or os.getenv("PGUSER")
PGPASSWORD = os.getenv("POSTGRES_PASSWORD") or os.getenv("PGPASSWORD")
def require_env():
missing = []
for name, value in {
"ODOO_URL": ODOO_URL,
"ODOO_DB/ODOO_DB_OVERRIDE": ODOO_DB,
"ODOO_USER": ODOO_USER,
"ODOO_PASSWORD": ODOO_PASSWORD,
"POSTGRES_DB": PGDATABASE,
"POSTGRES_USER": PGUSER,
"POSTGRES_PASSWORD": PGPASSWORD,
}.items():
if not value:
missing.append(name)
if missing:
raise RuntimeError("Faltan variables de entorno: " + ", ".join(missing))
def odoo_connect():
common = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/common", allow_none=True)
uid = common.authenticate(ODOO_DB, ODOO_USER, ODOO_PASSWORD, {})
if not uid:
raise RuntimeError("No se pudo autenticar contra Odoo")
models = xmlrpc.client.ServerProxy(f"{ODOO_URL}/xmlrpc/2/object", allow_none=True)
return uid, models
def odoo_call(models, uid, model, method, *args, **kwargs):
return models.execute_kw(ODOO_DB, uid, ODOO_PASSWORD, model, method, list(args), kwargs or {})
def pg_connect():
return psycopg2.connect(
host=PGHOST,
port=PGPORT,
dbname=PGDATABASE,
user=PGUSER,
password=PGPASSWORD,
)
def as_id(value):
if isinstance(value, list) and value:
return value[0]
if isinstance(value, tuple) and value:
return value[0]
if isinstance(value, int):
return value
return None
def as_name(value):
if isinstance(value, list) and len(value) > 1:
return value[1]
if isinstance(value, tuple) and len(value) > 1:
return value[1]
return None
def read_map_by_ids(models, uid, model, ids, fields):
ids = [int(x) for x in ids if x]
if not ids:
return {}
rows = odoo_call(models, uid, model, "read", ids, fields=fields)
return {r["id"]: r for r in rows}
def extract_default_code(display_name):
if not display_name:
return None
if display_name.startswith("[") and "]" in display_name:
return display_name[1:display_name.index("]")]
return None
def build_row(prod, product, sale_line, sale_order, raw_moves, finished_moves):
product_name = as_name(prod.get("product_id")) or (product or {}).get("display_name")
product_default_code = (product or {}).get("default_code") or extract_default_code(product_name)
product_standard_price = product.get("standard_price") if product else None
product_list_price = product.get("list_price") if product else None
product_uom = as_name((product or {}).get("uom_id")) or as_name(prod.get("product_uom_id"))
sale_order_ref = None
sale_order_id = None
customer_id = None
customer_name = None
sale_state = None
sale_date_order = None
currency = None
sale_line_qty = None
sale_line_qty_delivered = None
sale_price_unit = None
sale_discount_pct = None
revenue_net_eur = None
revenue_total_eur = None
sale_line_id = None
if sale_line:
sale_line_id = sale_line.get("id")
sale_line_qty = sale_line.get("product_uom_qty")
sale_line_qty_delivered = sale_line.get("qty_delivered")
sale_price_unit = sale_line.get("price_unit")
sale_discount_pct = sale_line.get("discount")
revenue_net_eur = sale_line.get("price_subtotal")
revenue_total_eur = sale_line.get("price_total")
if sale_order:
sale_order_id = sale_order.get("id")
sale_order_ref = sale_order.get("name")
sale_state = sale_order.get("state")
sale_date_order = sale_order.get("date_order")
customer_id = as_id(sale_order.get("partner_id"))
customer_name = as_name(sale_order.get("partner_id"))
currency = as_name(sale_order.get("currency_id"))
raw_material_value_eur = sum([float(m.get("value") or 0.0) for m in raw_moves])
finished_value_eur = sum([float(m.get("value") or 0.0) for m in finished_moves])
if raw_material_value_eur and raw_material_value_eur != 0:
material_cost_status = "FROM_ODOO_STOCK_MOVE_VALUE"
else:
material_cost_status = "UNAVAILABLE_OR_ZERO"
raw_json = {
"production": prod,
"product": product,
"sale_line": sale_line,
"sale_order": sale_order,
}
return {
"tenant": TENANT,
"site": SITE,
"odoo_instance": ODOO_INSTANCE,
"odoo_production_id": prod.get("id"),
"odoo_order_ref": prod.get("name"),
"odoo_origin": prod.get("origin"),
"odoo_state": prod.get("state"),
"product_id": as_id(prod.get("product_id")),
"product_default_code": product_default_code,
"product_name": product_name,
"product_uom": product_uom,
"product_standard_price": product_standard_price,
"product_list_price": product_list_price,
"production_qty": prod.get("product_qty"),
"qty_producing": prod.get("qty_producing"),
"production_uom": as_name(prod.get("product_uom_id")),
"sale_line_id": sale_line_id,
"sale_order_id": sale_order_id,
"sale_order_ref": sale_order_ref,
"customer_id": customer_id,
"customer_name": customer_name,
"sale_state": sale_state,
"sale_date_order": sale_date_order,
"currency": currency,
"sale_line_qty": sale_line_qty,
"sale_line_qty_delivered": sale_line_qty_delivered,
"sale_price_unit": sale_price_unit,
"sale_discount_pct": sale_discount_pct,
"revenue_net_eur": revenue_net_eur,
"revenue_total_eur": revenue_total_eur,
"raw_material_value_eur": raw_material_value_eur,
"finished_value_eur": finished_value_eur,
"material_cost_status": material_cost_status,
"stock_raw_moves_json": raw_moves,
"stock_finished_moves_json": finished_moves,
"raw_json": raw_json,
}
def upsert_rows(conn, rows):
if not rows:
return 0
sql = """
INSERT INTO mv_edge_oee_ucepsa_prod.odoo_order_commercials (
tenant,
site,
odoo_instance,
odoo_production_id,
odoo_order_ref,
odoo_origin,
odoo_state,
product_id,
product_default_code,
product_name,
product_uom,
product_standard_price,
product_list_price,
production_qty,
qty_producing,
production_uom,
sale_line_id,
sale_order_id,
sale_order_ref,
customer_id,
customer_name,
sale_state,
sale_date_order,
currency,
sale_line_qty,
sale_line_qty_delivered,
sale_price_unit,
sale_discount_pct,
revenue_net_eur,
revenue_total_eur,
raw_material_value_eur,
finished_value_eur,
material_cost_status,
stock_raw_moves_json,
stock_finished_moves_json,
raw_json,
snapshot_at
)
VALUES (
%(tenant)s,
%(site)s,
%(odoo_instance)s,
%(odoo_production_id)s,
%(odoo_order_ref)s,
%(odoo_origin)s,
%(odoo_state)s,
%(product_id)s,
%(product_default_code)s,
%(product_name)s,
%(product_uom)s,
%(product_standard_price)s,
%(product_list_price)s,
%(production_qty)s,
%(qty_producing)s,
%(production_uom)s,
%(sale_line_id)s,
%(sale_order_id)s,
%(sale_order_ref)s,
%(customer_id)s,
%(customer_name)s,
%(sale_state)s,
%(sale_date_order)s,
%(currency)s,
%(sale_line_qty)s,
%(sale_line_qty_delivered)s,
%(sale_price_unit)s,
%(sale_discount_pct)s,
%(revenue_net_eur)s,
%(revenue_total_eur)s,
%(raw_material_value_eur)s,
%(finished_value_eur)s,
%(material_cost_status)s,
%(stock_raw_moves_json)s,
%(stock_finished_moves_json)s,
%(raw_json)s,
now()
)
ON CONFLICT (odoo_instance, odoo_production_id)
DO UPDATE SET
tenant = EXCLUDED.tenant,
site = EXCLUDED.site,
odoo_order_ref = EXCLUDED.odoo_order_ref,
odoo_origin = EXCLUDED.odoo_origin,
odoo_state = EXCLUDED.odoo_state,
product_id = EXCLUDED.product_id,
product_default_code = EXCLUDED.product_default_code,
product_name = EXCLUDED.product_name,
product_uom = EXCLUDED.product_uom,
product_standard_price = EXCLUDED.product_standard_price,
product_list_price = EXCLUDED.product_list_price,
production_qty = EXCLUDED.production_qty,
qty_producing = EXCLUDED.qty_producing,
production_uom = EXCLUDED.production_uom,
sale_line_id = EXCLUDED.sale_line_id,
sale_order_id = EXCLUDED.sale_order_id,
sale_order_ref = EXCLUDED.sale_order_ref,
customer_id = EXCLUDED.customer_id,
customer_name = EXCLUDED.customer_name,
sale_state = EXCLUDED.sale_state,
sale_date_order = EXCLUDED.sale_date_order,
currency = EXCLUDED.currency,
sale_line_qty = EXCLUDED.sale_line_qty,
sale_line_qty_delivered = EXCLUDED.sale_line_qty_delivered,
sale_price_unit = EXCLUDED.sale_price_unit,
sale_discount_pct = EXCLUDED.sale_discount_pct,
revenue_net_eur = EXCLUDED.revenue_net_eur,
revenue_total_eur = EXCLUDED.revenue_total_eur,
raw_material_value_eur = EXCLUDED.raw_material_value_eur,
finished_value_eur = EXCLUDED.finished_value_eur,
material_cost_status = EXCLUDED.material_cost_status,
stock_raw_moves_json = EXCLUDED.stock_raw_moves_json,
stock_finished_moves_json = EXCLUDED.stock_finished_moves_json,
raw_json = EXCLUDED.raw_json,
snapshot_at = now()
"""
prepared = []
for row in rows:
r = dict(row)
r["stock_raw_moves_json"] = psycopg2.extras.Json(r["stock_raw_moves_json"])
r["stock_finished_moves_json"] = psycopg2.extras.Json(r["stock_finished_moves_json"])
r["raw_json"] = psycopg2.extras.Json(r["raw_json"])
prepared.append(r)
with conn.cursor() as cur:
psycopg2.extras.execute_batch(cur, sql, prepared, page_size=50)
return len(rows)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--production-id", type=int, action="append", help="ID Odoo mrp.production concreto")
parser.add_argument("--order-ref", action="append", help="Referencia WH/MO/xxxxx concreta")
parser.add_argument("--limit", type=int, default=50)
parser.add_argument("--states", default="confirmed,progress,to_close,done", help="Estados mrp.production separados por coma")
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args()
require_env()
uid, models = odoo_connect()
print(f"Conectado a Odoo: url={ODOO_URL} db={ODOO_DB} user={ODOO_USER} uid={uid}", flush=True)
domain = []
if args.production_id:
domain.append(("id", "in", args.production_id))
elif args.order_ref:
domain.append(("name", "in", args.order_ref))
else:
states = [s.strip() for s in args.states.split(",") if s.strip()]
domain.append(("state", "in", states))
prod_fields = [
"id",
"name",
"state",
"origin",
"product_id",
"product_qty",
"qty_producing",
"product_uom_id",
"bom_id",
"move_raw_ids",
"move_finished_ids",
"sale_line_id",
"date_start",
"date_finished",
"company_id",
]
production_ids = odoo_call(
models,
uid,
"mrp.production",
"search",
domain,
limit=args.limit,
order="id desc",
)
print(f"Producciones encontradas: {len(production_ids)} -> {production_ids}", flush=True)
if not production_ids:
return 0
productions = odoo_call(models, uid, "mrp.production", "read", production_ids, fields=prod_fields)
product_ids = [as_id(p.get("product_id")) for p in productions if as_id(p.get("product_id"))]
sale_line_ids = [as_id(p.get("sale_line_id")) for p in productions if as_id(p.get("sale_line_id"))]
raw_move_ids = []
finished_move_ids = []
for p in productions:
raw_move_ids.extend(p.get("move_raw_ids") or [])
finished_move_ids.extend(p.get("move_finished_ids") or [])
products = read_map_by_ids(
models,
uid,
"product.product",
product_ids,
[
"id",
"display_name",
"default_code",
"standard_price",
"list_price",
"uom_id",
"categ_id",
"barcode",
"product_tmpl_id",
],
)
sale_lines = read_map_by_ids(
models,
uid,
"sale.order.line",
sale_line_ids,
[
"id",
"name",
"order_id",
"product_id",
"product_uom_qty",
"qty_delivered",
"price_unit",
"discount",
"price_subtotal",
"price_total",
],
)
sale_order_ids = [as_id(sl.get("order_id")) for sl in sale_lines.values() if as_id(sl.get("order_id"))]
sale_orders = read_map_by_ids(
models,
uid,
"sale.order",
sale_order_ids,
[
"id",
"name",
"state",
"origin",
"date_order",
"partner_id",
"currency_id",
"amount_total",
"amount_untaxed",
"client_order_ref",
"order_line",
],
)
move_fields = [
"id",
"state",
"value",
"quantity",
"price_unit",
"product_id",
"product_uom",
"production_id",
"product_uom_qty",
"raw_material_production_id",
]
raw_moves_by_id = read_map_by_ids(models, uid, "stock.move", raw_move_ids, move_fields)
finished_moves_by_id = read_map_by_ids(models, uid, "stock.move", finished_move_ids, move_fields)
rows = []
for p in productions:
product = products.get(as_id(p.get("product_id")))
sale_line = sale_lines.get(as_id(p.get("sale_line_id")))
sale_order = sale_orders.get(as_id(sale_line.get("order_id"))) if sale_line else None
raw_moves = [raw_moves_by_id[mid] for mid in (p.get("move_raw_ids") or []) if mid in raw_moves_by_id]
finished_moves = [finished_moves_by_id[mid] for mid in (p.get("move_finished_ids") or []) if mid in finished_moves_by_id]
row = build_row(p, product, sale_line, sale_order, raw_moves, finished_moves)
rows.append(row)
print(json.dumps(
[
{
"odoo_order_ref": r["odoo_order_ref"],
"odoo_production_id": r["odoo_production_id"],
"product": r["product_name"],
"sale_order_ref": r["sale_order_ref"],
"customer_name": r["customer_name"],
"revenue_net_eur": r["revenue_net_eur"],
"raw_moves": len(r["stock_raw_moves_json"]),
"finished_moves": len(r["stock_finished_moves_json"]),
"material_cost_status": r["material_cost_status"],
}
for r in rows
],
indent=2,
ensure_ascii=False,
default=str,
), flush=True)
if args.dry_run:
print("DRY RUN: no se escribe en PostgreSQL", flush=True)
return 0
with pg_connect() as conn:
count = upsert_rows(conn, rows)
conn.commit()
print(f"Filas insertadas/actualizadas en mv_edge_oee_ucepsa_prod.odoo_order_commercials: {count}", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())