#!/usr/bin/env python3 """Seed demo data for the admin "Reports" section. Creates / completes a spread of ``encoach.student.attempt`` records across two entities so that: * /admin/student-performance shows multiple rows with different CEFR levels * /admin/stats-corporate has attempts in multiple months + entities * /admin/record shows per-user attempt history Idempotent: re-running only fills in missing attempts — it never wipes data. Usage: BACKEND_DIR=$PWD/backend \\ /path/to/python backend/odoo/odoo-bin shell \\ -c backend/custom_addons/encoach_demo.conf \\ --no-http --stop-after-init < seed_reports.py In practice this repo has a helper ``run.sh seed seed_reports.py``. """ # pylint: disable=undefined-variable import random from datetime import datetime, timedelta try: env # noqa: F821 - injected by odoo-bin shell except NameError: raise SystemExit('Run this inside odoo-bin shell (env not available).') def _or_create_entity(name, code): rec = env['encoach.entity'].search([('name', '=', name)], limit=1) if rec: return rec rec = env['encoach.entity'].search([('code', '=', code)], limit=1) if rec: return rec return env['encoach.entity'].create({'name': name, 'code': code}) def _students(): Users = env['res.users'].sudo() logins = ['sarah@encoach.test', 'omar@encoach.test', 'layla@encoach.test', 'testuser@writeflow.test'] out = [] for login in logins: u = Users.search([('login', '=', login)], limit=1) if u: out.append(u) return out def _default_exam(): exam = env['encoach.exam.custom'].sudo().search([], order='id', limit=1) if exam: return exam # Fallback: create a minimal exam shell if none exists. return env['encoach.exam.custom'].sudo().create({ 'title': 'Seeded Demo Exam', 'status': 'active', }) BAND_MATRIX = [ # listening, reading, writing, speaking -> overall auto (avg) (7.5, 8.0, 7.0, 7.5, 'C1'), (6.0, 6.5, 5.5, 6.0, 'B2'), (5.0, 5.5, 4.5, 5.0, 'B1'), (4.0, 4.5, 4.0, 4.0, 'A2'), (8.0, 8.5, 8.0, 8.5, 'C1'), (6.5, 7.0, 6.5, 6.5, 'B2'), ] def _avg(l, r, w, s): return round((l + r + w + s) / 4.0, 1) def seed(): acme = _or_create_entity('Acme Corp', 'ACME') globe = _or_create_entity('Global Ltd', 'GLOBAL') tech = _or_create_entity('Tech Co', 'TECHCO') entities = [acme, globe, tech] students = _students() if not students: print('[seed_reports] No students found — nothing to seed.') return exam = _default_exam() print(f'[seed_reports] Using exam id={exam.id} title={exam.title!r}') Att = env['encoach.student.attempt'].sudo() # 1. Fill in bands for any existing in_progress attempts that have no scores. stale = Att.search([('status', '=', 'in_progress')]) for idx, att in enumerate(stale): vals = BAND_MATRIX[idx % len(BAND_MATRIX)] l, r, w, s, cefr = vals started = att.started_at or (datetime.now() - timedelta(days=5)) att.write({ 'status': 'completed', 'listening_band': l, 'reading_band': r, 'writing_band': w, 'speaking_band': s, 'overall_band': _avg(l, r, w, s), 'cefr_level': cefr.lower().replace('-', '_'), 'completed_at': started + timedelta(minutes=120), 'entity_id': att.entity_id.id or random.choice(entities).id, }) print(f'[seed_reports] Completed {len(stale)} existing in_progress attempts.') # 2. Add historical attempts across the last 6 months so the trend chart # has enough data points. now = datetime.now() created = 0 for month_ago in range(5, -1, -1): ref = now - timedelta(days=30 * month_ago + random.randint(0, 15)) for i, student in enumerate(students): # Skip ~30% of (student, month) pairs so the trend varies. if random.random() < 0.3 and month_ago > 0: continue # Slight progression over time so trend line rises. base = BAND_MATRIX[(i + month_ago) % len(BAND_MATRIX)] l, r, w, s, cefr = base delta = max(0.0, (6 - month_ago) * 0.2) l = min(9.0, l + delta) r = min(9.0, r + delta) w = min(9.0, w + delta) s = min(9.0, s + delta) entity = entities[(i + month_ago) % len(entities)] existing = Att.search([ ('student_id', '=', student.id), ('exam_id', '=', exam.id), ('started_at', '>=', ref - timedelta(days=3)), ('started_at', '<=', ref + timedelta(days=3)), ], limit=1) if existing: continue vals = { 'student_id': student.id, 'exam_id': exam.id, 'entity_id': entity.id, 'status': 'completed', 'started_at': ref, 'completed_at': ref + timedelta(minutes=100 + i * 15), 'listening_band': l, 'reading_band': r, 'writing_band': w, 'speaking_band': s, 'overall_band': _avg(l, r, w, s), 'cefr_level': cefr.lower().replace('-', '_'), } Att.create(vals) created += 1 print(f'[seed_reports] Created {created} historical attempts.') env.cr.commit() total_reportable = Att.search_count([('status', 'in', ['completed', 'scored', 'released', 'scoring'])]) print(f'[seed_reports] OK. Reportable attempts now: {total_reportable}') seed()