#!/usr/bin/env python3 """Backfill marketplace_listings.inventory_id for a tenant (MercadoLibre). For every listing already imported from MercadoLibre, fetch the ML item and match it against local inventory by: 1. seller_custom_field / seller_sku 2. SELLER_SKU / PART_NUMBER / SELLER_PART_NUMBER attributes (item level and per variation) 3. inventory.part_number, then inventory_sku_aliases.sku (exact match first, then normalized: spaces/dashes/dots stripped) 4. the ML item id itself, as a last resort (some tenants store it as alias) Dry-run by default: it reports what it *would* map without writing anything. Pass --apply to persist inventory_id + inventory_match_source + matched_at. Usage (from /home/Autopartes): python3 scripts/backfill_meli_listings.py --tenant 31 # dry-run python3 scripts/backfill_meli_listings.py --tenant 31 --apply python3 scripts/backfill_meli_listings.py --tenant 31 --apply --limit 10 python3 scripts/backfill_meli_listings.py --tenant 31 --all # re-check mapped too python3 scripts/backfill_meli_listings.py --tenant 31 --json Requires MASTER_DB_URL + TENANT_DB_URL_TEMPLATE in the environment; if missing they are loaded from /etc/nexus/pos.env. """ import argparse import json import os import sys sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'pos')) def _load_env(): env_file = '/etc/nexus/pos.env' if os.environ.get('MASTER_DB_URL') or not os.path.exists(env_file): return for line in open(env_file): line = line.strip() if not line or line.startswith('#') or '=' not in line: continue key, value = line.split('=', 1) os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('--tenant', type=int, required=True, help='tenant id (e.g. 31 = Rached)') ap.add_argument('--apply', action='store_true', help='write the matches (default: dry-run, read-only)') ap.add_argument('--limit', type=int, default=None, help='max listings to process') ap.add_argument('--all', action='store_true', help='also re-check listings that already have an inventory_id') ap.add_argument('--json', action='store_true', help='print the full summary as JSON') args = ap.parse_args() _load_env() from tenant_db import get_tenant_conn from services import marketplace_external_service as meli conn = get_tenant_conn(args.tenant) try: summary = meli.backfill_listing_inventory( conn, limit=args.limit, only_unmapped=not args.all, dry_run=not args.apply, ) finally: conn.close() if args.json: print(json.dumps(summary, indent=2, ensure_ascii=False, default=str)) return 0 mode = 'DRY-RUN (no writes)' if summary['dry_run'] else 'APPLIED' print(f"[tenant {args.tenant}] backfill inventory_id — {mode}") print(f" scanned: {summary['scanned']}") print(f" matched: {summary['matched']}") print(f" unmatched: {summary['unmatched']}") print(f" errors: {summary['errors']}") unmatched = [d for d in summary['details'] if d.get('status') == 'unmatched'] if unmatched: print("\n Unmatched listings (ML item has no local part number/SKU):") for d in unmatched: cands = d.get('candidates') or [] print(f" listing {d['listing_id']} ({d['external_item_id']}) " f"{(d.get('title') or '')[:45]!r} candidates={cands}") if summary['errors']: print("\n Errors:") for d in summary['details']: if d.get('status') == 'error': print(f" listing {d['listing_id']}: {d.get('error')}") if summary['dry_run'] and summary['matched']: print("\n Re-run with --apply to persist these matches.") return 0 if __name__ == '__main__': sys.exit(main())