Import ClientFlow production v4928.1.5.132.4
This commit is contained in:
92
scripts/audit_odoo_opportunity_sync.py
Executable file
92
scripts/audit_odoo_opportunity_sync.py
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only audit for Odoo status consistency on ClientFlow opportunities.
|
||||
|
||||
Reports opportunities that are in advanced operational stages but lack a linked
|
||||
Odoo sale order or a recently synced physical_status operation link.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.db import engine
|
||||
|
||||
ADVANCED_STAGES = (
|
||||
"ODOO_ORDER_CREATED", "IN_PRODUCTION", "ORDER_PREPARATION", "READY_TO_SHIP",
|
||||
"INVOICED", "SHIPMENT_CREATED", "SHIPPED", "TRACKING_SENT", "DELIVERED", "WON",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(text("""
|
||||
SELECT
|
||||
o.id::text AS opportunity_id,
|
||||
o.title,
|
||||
o.customer_name,
|
||||
o.customer_email,
|
||||
o.status,
|
||||
o.stage,
|
||||
sale.external_name AS odoo_sale,
|
||||
sale.status AS sale_status,
|
||||
phys.status AS physical_status,
|
||||
phys.last_synced_at AS physical_last_synced_at,
|
||||
inv.external_name AS invoice_number
|
||||
FROM opportunities o
|
||||
LEFT JOIN operation_links sale
|
||||
ON sale.opportunity_id = o.id
|
||||
AND sale.system = 'odoo'
|
||||
AND sale.external_type = 'sale_order'
|
||||
LEFT JOIN operation_links phys
|
||||
ON phys.opportunity_id = o.id
|
||||
AND phys.system = 'odoo'
|
||||
AND phys.external_type = 'physical_status'
|
||||
LEFT JOIN operation_links inv
|
||||
ON inv.opportunity_id = o.id
|
||||
AND inv.system = 'jasmin'
|
||||
AND inv.external_type = 'invoice'
|
||||
WHERE COALESCE(o.status, '') IN ('open','won','closed','done','completed')
|
||||
AND (
|
||||
COALESCE(o.stage, '') = ANY(CAST(:advanced AS text[]))
|
||||
OR inv.id IS NOT NULL
|
||||
)
|
||||
ORDER BY o.updated_at DESC
|
||||
LIMIT 200
|
||||
"""), {"advanced": list(ADVANCED_STAGES)}).mappings().all()
|
||||
|
||||
missing_sale = [dict(r) for r in rows if not r.get("odoo_sale")]
|
||||
missing_physical = [dict(r) for r in rows if r.get("odoo_sale") and not r.get("physical_status")]
|
||||
|
||||
report = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"checked": len(rows),
|
||||
"missing_sale_order": missing_sale[:50],
|
||||
"missing_physical_status": missing_physical[:50],
|
||||
}
|
||||
out_dir = Path("audit_reports")
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
out_path = out_dir / "odoo_opportunity_sync_audit.json"
|
||||
out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2, default=str))
|
||||
|
||||
print("ClientFlow Odoo opportunity sync audit")
|
||||
print("=" * 80)
|
||||
print("checked:", len(rows))
|
||||
print("missing_sale_order:", len(missing_sale))
|
||||
print("missing_physical_status:", len(missing_physical))
|
||||
print("JSON:", out_path)
|
||||
if missing_sale:
|
||||
print("\nExamples missing sale order:")
|
||||
for r in missing_sale[:10]:
|
||||
print(f"- {r['opportunity_id']} | {r.get('title')} | stage={r.get('stage')} | invoice={r.get('invoice_number')}")
|
||||
if missing_physical:
|
||||
print("\nExamples missing physical status:")
|
||||
for r in missing_physical[:10]:
|
||||
print(f"- {r['opportunity_id']} | {r.get('title')} | sale={r.get('odoo_sale')} | stage={r.get('stage')}")
|
||||
return 1 if missing_sale or missing_physical else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user