"""Shared JSON-RPC helpers for the standalone migration scripts in this directory (migrate_members.py, migrate_students.py, migrate_opening_balances.py, migrate_classroom.py). Deliberately stdlib-only (no external dependencies) so these scripts can run on any machine with Python 3, without needing the project's own virtualenv or the Odoo MCP server - they talk to a deployed Odoo instance purely over JSON-RPC. """ import csv import json import re import urllib.request from datetime import datetime def jsonrpc_call(url, service, method, args): payload = { 'jsonrpc': '2.0', 'method': 'call', 'params': {'service': service, 'method': method, 'args': args}, } request = urllib.request.Request( f'{url}/jsonrpc', data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json'}, ) with urllib.request.urlopen(request) as response: result = json.loads(response.read()) if 'error' in result: raise RuntimeError(result['error'].get('data', {}).get('message') or result['error']) return result['result'] def authenticate(url, db, username, password): uid = jsonrpc_call(url, 'common', 'authenticate', [db, username, password, {}]) if not uid: raise RuntimeError('Authentication failed - check --url/--db/--username/--password') return uid def execute_kw(url, db, uid, password, model, method, args, kwargs=None): full_args = [db, uid, password, model, method, args] if kwargs is not None: return jsonrpc_call(url, 'object', 'execute_kw', full_args + [kwargs]) return jsonrpc_call(url, 'object', 'execute_kw', full_args) def normalize_email(value): return (value or '').strip().lower() or None def normalize_phone(value): value = (value or '').strip() if not value: return None keep_plus = value.startswith('+') digits = re.sub(r'\D', '', value) return ('+' if keep_plus else '') + digits def normalize_date(value): """Parse a handful of common export date formats into ISO (YYYY-MM-DD).""" value = (value or '').strip() if not value: return None for fmt in ('%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%Y/%m/%d', '%B %d, %Y', '%b %d, %Y'): try: return datetime.strptime(value, fmt).date().isoformat() except ValueError: continue return None class MigrationReport: """Accumulates per-row outcomes and writes them to a report CSV.""" def __init__(self): self.rows = [] def add(self, key, action, reason=''): self.rows.append({'key': key, 'action': action, 'reason': reason}) def write(self, path): with open(path, 'w', newline='', encoding='utf-8') as handle: writer = csv.DictWriter(handle, fieldnames=['key', 'action', 'reason']) writer.writeheader() writer.writerows(self.rows) def summary(self): counts = {} for row in self.rows: counts[row['action']] = counts.get(row['action'], 0) + 1 return counts