TNCSC_Odoo/scripts/_migration_common.py
metatroncubeswdev 8c8043a49a docs: add HANDOFF.md, commit Phase 8 WIP
HANDOFF.md is the "where things actually stand" companion to the plan
document: a phase-by-phase status table, how to spin the dev environment
back up (including the "restart the container after any module change or
you'll hit a stale registry" gotcha that bit repeatedly this session), an
index of every real Odoo 19 API-drift gotcha discovered during the build
(pointing at the commit that documents each in full rather than
duplicating it), and instructions for pushing this repo to a remote before
handing it to a team (none is configured yet - this repo only exists
locally).

Also commits the two Phase 8 files that existed only as uncommitted local
changes (scripts/_migration_common.py, a first pass at
scripts/migrate_members.py) so they survive a git clone rather than being
fragile local-only WIP. Neither is wired into anything or tested yet -
migrate_members.py has no live run against Odoo, and migrate_students.py /
migrate_opening_balances.py don't exist yet. Paused here at the user's
request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 07:29:29 -04:00

95 lines
3.0 KiB
Python

"""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