#!/usr/bin/env python3 """ seed_dynamic_sections_demo.py — Idempotent demo data for the dynamic Course → Sections → Classroom → Batch structure. Reproduces the user diagrams literally: Courses & Sections Classrooms ────────────────── ─────────── Math 101 ─┬─ Section A Class A1010 hosts: ├─ Section B • Math 101 — Section A └─ Section C • Physics 102 — Section B Physics 102 ─┬─ Section A ├─ Section B Class A1030 hosts: └─ Section C • Math 101 — Section B • Physics 102 — Section C Plus the entity-aware homeroom layer: Entity (DEMO_ACADEMY) └─ Classroom (A1010 / A1030) ├─ Students (homeroom roster) ← Sarah / Omar / Layla / TestUser ├─ Teachers (homeroom M2M) ← Dr. Khalid + Ms. Fatima ├─ Courses (M2M) ← Math 101 + Physics 102 └─ Batches (auto, one per ← cascade from assign_course() classroom × course × section × term) Run inside Odoo shell: cd /Users/yamenahmad/projects2026/odoo/odoo19 .conda-envs/odoo19/bin/python odoo/odoo-bin shell -c odoo.conf -d encoach_v2 \ < seed_dynamic_sections_demo.py It expects `seed_demo.py` (and ideally `seed_full_demo.py`) to have run first so the demo entity, students, and teachers exist. It is safe to re-run any number of times — every step looks up before creating. """ from datetime import date print("\n" + "=" * 72) print(" EnCoach — Dynamic course-section demo seed (idempotent)") print("=" * 72) Users = env['res.users'] Entity = env['encoach.entity'] OpCourse = env['op.course'] OpStudent = env['op.student'] OpFaculty = env['op.faculty'] Classroom = env['op.classroom'] Section = env['encoach.course.section'] Batch = env['op.batch'] # ── 1. Resolve the demo entity ──────────────────────────────────────────── entity = Entity.search([('code', '=', 'DEMO_ACADEMY')], limit=1) \ or Entity.search([], limit=1) if not entity: raise SystemExit("✗ No encoach.entity found. Run seed_demo.py first.") print(f"✓ Using entity: {entity.name} (id={entity.id})") # ── 2. Resolve demo students + homeroom teachers by login ───────────────── def _student_for(login): user = Users.search([('login', '=', login)], limit=1) if not user: return None if user.op_student_id: return user.op_student_id return OpStudent.search([('user_id', '=', user.id)], limit=1) or None def _faculty_for(login): user = Users.search([('login', '=', login)], limit=1) if not user: return None if user.op_faculty_id: return user.op_faculty_id return OpFaculty.search([('user_id', '=', user.id)], limit=1) or None sarah = _student_for('sarah@encoach.test') omar = _student_for('omar@encoach.test') layla = _student_for('layla@encoach.test') khalid = _faculty_for('khalid@encoach.test') fatima = _faculty_for('fatima@encoach.test') # Pick one extra student for A1030 — first non-(sarah/omar/layla) student available. extra_student = None already = {s.id for s in (sarah, omar, layla) if s} for cand in OpStudent.search([], limit=20): if cand.id not in already: extra_student = cand break print("✓ Resolved demo people:") print(f" students: sarah={sarah and sarah.id} omar={omar and omar.id} " f"layla={layla and layla.id} extra={extra_student and extra_student.id}") print(f" homeroom: khalid={khalid and khalid.id} fatima={fatima and fatima.id}") if not all([sarah, omar, layla, khalid, fatima]): print("⚠ Missing demo users — please run seed_demo.py first; continuing with what we have.") # ── 3. Create / reuse Math 101 & Physics 102 (entity-scoped) ────────────── def _ensure_course(name, code, description, capacity=40): course = OpCourse.search([('code', '=', code)], limit=1) if not course: vals = { 'name': name, 'code': code, 'description': description, 'max_capacity': capacity, 'evaluation_type': 'normal', } if 'entity_id' in OpCourse._fields: vals['entity_id'] = entity.id course = OpCourse.create(vals) print(f" + created course: {name} ({code}) id={course.id}") else: if 'entity_id' in OpCourse._fields and not course.entity_id: course.write({'entity_id': entity.id}) print(f" · reused course: {name} ({code}) id={course.id}") return course print("\n--- Courses ---") math = _ensure_course("Math 101", "MATH101", "Foundations of mathematics.", 40) physics = _ensure_course("Physics 102", "PHYS102", "Introductory physics.", 40) env.cr.commit() # ── 4. Generate A/B/C sections per course ───────────────────────────────── def _ensure_sections(course, codes=("A", "B", "C")): out = {} for idx, c in enumerate(codes, start=1): s = Section.search([('course_id', '=', course.id), ('code', '=', c)], limit=1) if not s: s = Section.create({ 'name': f"Section {c}", 'code': c, 'course_id': course.id, 'sequence': 10 * idx, 'active': True, }) print(f" + section {course.code}/{c} id={s.id}") else: print(f" · section {course.code}/{c} id={s.id} (existing)") out[c] = s return out print("\n--- Sections ---") math_secs = _ensure_sections(math) phys_secs = _ensure_sections(physics) env.cr.commit() # ── 5. Create / reuse classrooms (A1010, A1030) ─────────────────────────── def _ensure_classroom(name, code, capacity=35): rec = Classroom.search([('code', '=', code)], limit=1) if not rec: vals = { 'name': name, 'code': code, 'capacity': capacity, } if 'entity_id' in Classroom._fields: vals['entity_id'] = entity.id rec = Classroom.create(vals) print(f" + created classroom: {name} ({code}) id={rec.id}") else: if 'entity_id' in Classroom._fields and not rec.entity_id: rec.write({'entity_id': entity.id}) print(f" · reused classroom: {name} ({code}) id={rec.id}") return rec print("\n--- Classrooms ---") a1010 = _ensure_classroom("Class A1010", "A1010", 35) a1030 = _ensure_classroom("Class A1030", "A1030", 35) env.cr.commit() # ── 6. Roster + homeroom teachers (idempotent M2M add) ──────────────────── def _add_roster(classroom, students, teachers): s_cmds = [(4, s.id) for s in students if s] t_cmds = [(4, t.id) for t in teachers if t] vals = {} if s_cmds: vals['student_ids'] = s_cmds if t_cmds: vals['teacher_ids'] = t_cmds if vals: classroom.write(vals) print(f" · {classroom.name}: {len(classroom.student_ids)} student(s), " f"{len(classroom.teacher_ids)} homeroom teacher(s)") print("\n--- Rosters & homeroom teachers ---") _add_roster(a1010, [sarah, omar], [khalid, fatima]) # 2 students, 2 teachers _add_roster(a1030, [layla, extra_student], [khalid]) # 1-2 students, 1 teacher env.cr.commit() # ── 7. Bind sections to classrooms (creates batches + enrolls roster) ───── DIAGRAM = [ # (classroom, course, section_letter) (a1010, math, "A"), (a1010, physics, "B"), (a1030, math, "B"), (a1030, physics, "C"), ] TERM_NAME = "Semester 1 2026" TERM_KEY = "2026-T1" START = date(2026, 4, 27) END = date(2026, 12, 31) print("\n--- Section bindings (cascade → batches + auto-enroll) ---") for classroom, course, section_letter in DIAGRAM: section = (math_secs if course == math else phys_secs)[section_letter] teacher_ids = [t.id for t in (khalid, fatima) if t] batch = classroom.assign_course( course=course, term_name=TERM_NAME, start_date=START, end_date=END, teacher_ids=teacher_ids, section_id=section.id, term_key=TERM_KEY, ) print(f" ✓ {classroom.name:<14} → {course.name:<12} / Section {section_letter} " f"batch '{batch.name}' (code {batch.code}, {len(batch.student_ids)} students)") env.cr.commit() # ── 8. Final summary ────────────────────────────────────────────────────── print("\n" + "=" * 72) print(" Final state (matches the diagrams)") print("=" * 72) for classroom in (a1010, a1030): print(f"\n== {classroom.name} (code {classroom.code}, entity={classroom.entity_id.name}) ==") print(f" roster: {len(classroom.student_ids)} student(s) -> " f"{[s.name for s in classroom.student_ids]}") print(f" teachers: {[t.name for t in classroom.teacher_ids]} (homeroom)") print(f" courses: {[c.name for c in classroom.course_ids]}") for b in classroom.batch_ids.sorted('id'): sec_code = b.course_section_id.code if b.course_section_id else '-' print(f" batch: {b.course_id.name:<12} / Section {sec_code} -> " f"'{b.name}' ({len(b.student_ids)} students, term={b.term_key})") print("\n✓ seed_dynamic_sections_demo.py — done.") print(" Try the UI:") print(" /admin/courses -> Math 101 / Physics 102 each show 3 sections (A/B/C)") print(" /admin/classrooms -> Class A1010 + A1030 with rosters and 2 batches each") print(" /admin/batches -> 4 section-aware batches (Section column populated)")