#!/usr/bin/env python3 """Sync external systems into reconciliation candidates. Examples: PYTHONPATH=. python scripts/sync_external_reconciliation.py --all PYTHONPATH=. python scripts/sync_external_reconciliation.py --jasmin --limit 50 --days 3 PYTHONPATH=. python scripts/sync_external_reconciliation.py --odoo --days 3 PYTHONPATH=. python scripts/sync_external_reconciliation.py --customers --limit 200 The full --all pipeline first creates/updates fiscal customers from Jasmin/Odoo and then stages reconciliation_items. It never creates opportunities or confirms payments, so the operator can link/create/ignore in the UI. """ from __future__ import annotations import argparse import asyncio import json from typing import Any, Dict, List from app.external_reconciliation_sync import ( sync_all_external_reconciliation_candidates, sync_external_fiscal_customers_for_reconciliation, sync_jasmin_reconciliation_candidates, sync_odoo_reconciliation_candidates, sync_packlink_reconciliation_candidates, ) def _print(result: Dict[str, Any]) -> None: print(json.dumps(result, ensure_ascii=False, indent=2, default=str)) async def main() -> None: parser = argparse.ArgumentParser(description="Sync external APIs into ClientFlow reconciliation candidates.") parser.add_argument("--all", action="store_true", help="sync all enabled external systems") parser.add_argument("--jasmin", action="store_true", help="sync Jasmin quotations/invoices") parser.add_argument("--odoo", action="store_true", help="sync Odoo sale orders") parser.add_argument("--packlink", action="store_true", help="sync Packlink shipments") parser.add_argument("--customers", action="store_true", help="seed fiscal customers from Jasmin/Odoo before document reconciliation") parser.add_argument("--limit", type=int, default=50, help="maximum records per source") parser.add_argument("--days", type=int, default=3, help="lookback window for Jasmin/Odoo/Packlink syncs") args = parser.parse_args() if args.all or not (args.jasmin or args.odoo or args.packlink or args.customers): _print(await sync_all_external_reconciliation_candidates(limit=args.limit, days=args.days)) return if args.customers and not (args.jasmin or args.odoo or args.packlink): _print(await sync_external_fiscal_customers_for_reconciliation(limit=max(args.limit, 200))) return results: List[Dict[str, Any]] = [] customer_result = None if args.customers: customer_result = await sync_external_fiscal_customers_for_reconciliation(limit=max(args.limit, 200)) if args.jasmin: results.append(await sync_jasmin_reconciliation_candidates(limit=args.limit, days=args.days)) if args.odoo: # Odoo XML-RPC client is sync; the service itself stays sync for easier reuse. results.append(sync_odoo_reconciliation_candidates(limit=args.limit, days=args.days)) if args.packlink: results.append(await sync_packlink_reconciliation_candidates(limit=args.limit, days=args.days)) output = { "seen": sum(int(r.get("seen") or 0) for r in results), "created_or_updated": sum(int(r.get("created_or_updated") or 0) for r in results), "results": results, } if customer_result is not None: output["customer_seen"] = customer_result.get("seen", 0) output["customers_created_or_updated"] = customer_result.get("created_or_updated", 0) output["customer_results"] = customer_result.get("results", []) _print(output) if __name__ == "__main__": asyncio.run(main())