529 lines
16 KiB
Python
Executable File
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())
|