Release v4928.1.4.2 stable

This commit is contained in:
2026-06-09 22:55:58 +01:00
commit 6445044ac6
280 changed files with 41775 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Backfill Jasmin product mappings for Odoo-imported opportunity lines.
Maps Odoo line metadata product ids to the ClientFlow catalogue convention:
``product_id=3`` -> ``products.sku='ODOO-3'`` -> ``jasmin_sales_item``.
Usage:
PYTHONPATH=. python scripts/backfill_opportunity_product_mappings.py
PYTHONPATH=. python scripts/backfill_opportunity_product_mappings.py --opportunity-id <uuid> --apply
"""
from __future__ import annotations
import argparse
from sqlalchemy import text
from app.db import engine
from app.product_service import ensure_product_schema
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--opportunity-id", default="", help="Optional opportunity UUID to restrict the backfill")
parser.add_argument("--apply", action="store_true", help="Apply changes. Without this flag it runs as dry-run.")
args = parser.parse_args()
ensure_product_schema()
where_opp = "AND oi.opportunity_id = CAST(:opportunity_id AS UUID)" if args.opportunity_id else ""
params = {"opportunity_id": args.opportunity_id} if args.opportunity_id else {}
with engine.begin() as conn:
rows = conn.execute(text(f"""
SELECT
oi.id::text AS item_id,
oi.opportunity_id::text AS opportunity_id,
oi.product_name,
oi.sku AS old_sku,
oi.jasmin_sales_item AS old_jasmin_sales_item,
oi.metadata->>'product_id' AS odoo_product_id,
p.id::text AS product_id,
p.sku AS new_sku,
p.jasmin_sales_item AS new_jasmin_sales_item,
p.name AS catalog_name
FROM opportunity_items oi
JOIN products p
ON p.sku = ('ODOO-' || (oi.metadata->>'product_id'))
WHERE oi.metadata->>'source_system' = 'odoo'
AND COALESCE(oi.metadata->>'product_id','') <> ''
AND (
oi.product_id IS DISTINCT FROM p.id
OR COALESCE(oi.sku,'') IS DISTINCT FROM COALESCE(p.sku,'')
OR COALESCE(oi.jasmin_sales_item,'') IS DISTINCT FROM COALESCE(p.jasmin_sales_item,'')
)
{where_opp}
ORDER BY oi.created_at DESC
"""), params).mappings().all()
print(f"Candidatos a atualizar: {len(rows)}")
for row in rows:
print(dict(row))
if args.apply and rows:
result = conn.execute(text(f"""
UPDATE opportunity_items oi
SET product_id = p.id,
sku = p.sku,
jasmin_sales_item = p.jasmin_sales_item,
metadata = COALESCE(oi.metadata, '{{}}'::jsonb) || jsonb_build_object(
'resolved_sku', p.sku,
'resolved_jasmin_sales_item', p.jasmin_sales_item,
'product_mapping_status', CASE WHEN COALESCE(p.jasmin_sales_item,'') <> '' THEN 'mapped' ELSE 'missing_jasmin' END,
'catalog_name', p.name,
'backfilled_at', now()::text
),
updated_at = now()
FROM products p
WHERE p.sku = ('ODOO-' || (oi.metadata->>'product_id'))
AND oi.metadata->>'source_system' = 'odoo'
AND COALESCE(oi.metadata->>'product_id','') <> ''
{where_opp}
"""), params)
print(f"Aplicado: {result.rowcount or 0} linha(s) atualizada(s)")
elif not args.apply:
print("Dry-run. Usa --apply para aplicar.")
return 0
if __name__ == "__main__":
raise SystemExit(main())