Ship three fully-wired admin areas end-to-end with APIs, seeds, tests and docs. Backend (new `encoach_lms_api` addon + existing addons): - Institutional: academic years/terms, departments, admission registers & admissions, courses/batches, lessons, fees (terms + student fees + invoicing with income-account auto-wiring), gradebook (assignments/grades), library, facilities (encoach.asset), student leave, result templates + marksheets (incl. delete-with-cascade). - Support: `encoach.ticket` model + CRUD/assignee routes; payment records derived from `op.student.fees.details` and `account.move`; platform settings backed by `encoach.code` and `ir.config_parameter` (packages + grading config). - Training: `encoach.vocab.item` + `encoach.grammar.rule` (plus progress models) with CRUD, pagination, search/level filters, and upsert-style progress endpoints. Odoo 19 compatibility: `_sql_constraints` replaced with `@api.constrains`; `ValidationError`/`UserError` mapped to HTTP 400. Frontend: - Rewire institutional admin pages (Academic Year Manager, Admissions, Courses, Lessons, Fees, Gradebook, Library, Facilities, Student Leave, Marksheets, Taxonomy, Resources) to real APIs with React Query invalidation and dialogs. - New typed services: `payments.service.ts`, `platformSettings.service.ts`, `training.service.ts`. Updated `fees/gradebook/lms/courseware/taxonomy/ resources/student-progress/generation` services + related types. - Rewrite `VocabularyPage`, `GrammarPage`, `PaymentRecordPage`, `SettingsPage`, `TicketsPage` to consume live data with search/filter/progress/CRUD flows. - New shared components: `TaxonomyCascade`, `MaterialViewer`, `teacher/TeacherLibrary`. - Favicons/branding assets and misc. UX polish across teacher/student pages. Tooling & QA: - Seeders: `seed_demo.py`, `seed_demo_data.py`, `seed_institutional.py` (idempotent, covers institutional + support + training fixtures incl. income-account wiring). - API write-flow test suites: `test_write_flows.py` (institutional), `test_support_flows.py` (support), `test_training_flows.py` (training), `test_ai_full.py`. All suites pass end-to-end. - Docs: add `docs/PROJECT_SUMMARY.md` with per-section scope, artifacts and QA. - `.gitignore`: ignore `pgdata_bak_*/`, `frontend/.vite/`, `frontend/dist/`, `frontend/node_modules/`. Made-with: Cursor
631 lines
27 KiB
Python
631 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Seed institutional (OpenEduCat ERP) fixtures for EnCoach:
|
|
- Academic Year + Terms
|
|
- Departments
|
|
- Admission Register + 1 Admission
|
|
- Exam Type, Exam Session + Result Template
|
|
- Faculty, Facility, Activity (type + record)
|
|
- Library (Author, Publisher, Media)
|
|
- Lessons (encoach.lesson)
|
|
- Gradebook + lines
|
|
- Student Leave (type + request)
|
|
- Fees terms (op.fees.terms)
|
|
- Attendance + Assignment + Marksheet lines so Student Progress drill-down renders
|
|
|
|
Assumes seed_demo.py has ALREADY been run (admin user, op.student rows, op.course etc. exist).
|
|
|
|
Run inside Odoo shell:
|
|
python3 odoo/odoo-bin shell -c odoo.conf -d encoach_v2 < seed_institutional.py
|
|
"""
|
|
from datetime import date, datetime, timedelta
|
|
from odoo import fields
|
|
|
|
# ── 0. Lookups ───────────────────────────────────────────────────────────
|
|
OpCourse = env['op.course']
|
|
OpBatch = env['op.batch']
|
|
OpStudent = env['op.student']
|
|
OpFaculty = env['op.faculty']
|
|
OpSubject = env['op.subject']
|
|
Users = env['res.users']
|
|
|
|
course = OpCourse.search([], limit=1)
|
|
batch = OpBatch.search([], limit=1)
|
|
students = OpStudent.search([], limit=3)
|
|
faculty = OpFaculty.search([], limit=1)
|
|
subject = OpSubject.search([], limit=1)
|
|
if not subject:
|
|
subject = OpSubject.create({'name': 'English Language', 'code': 'ENGX'})
|
|
admin = Users.search([('login', '=', 'admin')], limit=1) or Users.search([('login', '=', 'admin@encoach.test')], limit=1)
|
|
|
|
assert course, "No op.course present — run seed_demo.py first"
|
|
assert batch, "No op.batch present — run seed_demo.py first"
|
|
assert students, "No op.student present — run seed_demo.py first"
|
|
print(f"✓ Found base records: course={course.name}, batch={batch.name}, students={len(students)}")
|
|
|
|
# ── 1. Academic Year + Terms ─────────────────────────────────────────────
|
|
AcYear = env['op.academic.year']
|
|
AcTerm = env['op.academic.term']
|
|
|
|
year = AcYear.search([('name', '=', 'AY 2026')], limit=1)
|
|
if not year:
|
|
year = AcYear.create({
|
|
'name': 'AY 2026',
|
|
'start_date': '2026-01-01',
|
|
'end_date': '2026-12-31',
|
|
'term_structure': 'two_sem',
|
|
})
|
|
if not year.academic_term_ids:
|
|
year.term_create()
|
|
env.cr.commit()
|
|
print(f"✓ Academic Year: {year.name} with {len(year.academic_term_ids)} terms")
|
|
|
|
# ── 2. Departments ───────────────────────────────────────────────────────
|
|
OpDept = env['op.department']
|
|
dept_data = [
|
|
{'name': 'English Language Department', 'code': 'ELD'},
|
|
{'name': 'IELTS Preparation Unit', 'code': 'IPU'},
|
|
{'name': 'Academic Administration', 'code': 'ADM'},
|
|
]
|
|
for d in dept_data:
|
|
if not OpDept.search([('code', '=', d['code'])], limit=1):
|
|
OpDept.create(d)
|
|
env.cr.commit()
|
|
print(f"✓ Departments: {OpDept.search_count([])}")
|
|
|
|
# ── 3. Admission Register + 1 Admission ──────────────────────────────────
|
|
AdmReg = env['op.admission.register']
|
|
Adm = env['op.admission']
|
|
|
|
reg = AdmReg.search([('name', '=', 'IELTS Intake Spring 2026')], limit=1)
|
|
if not reg:
|
|
reg = AdmReg.create({
|
|
'name': 'IELTS Intake Spring 2026',
|
|
'start_date': '2026-02-01',
|
|
'end_date': '2026-04-30',
|
|
'course_id': course.id,
|
|
'min_count': 1,
|
|
'max_count': 50,
|
|
'academic_years_id': year.id,
|
|
'academic_term_id': year.academic_term_ids[:1].id if year.academic_term_ids else False,
|
|
'state': 'confirm',
|
|
})
|
|
env.cr.commit()
|
|
|
|
# Attach 1 admission driven from the first student
|
|
if students and not Adm.search([('register_id', '=', reg.id)], limit=1):
|
|
s0 = students[0]
|
|
Adm.create({
|
|
'name': f'{s0.first_name or "Sarah"} {s0.last_name or "Ahmed"}',
|
|
'first_name': s0.first_name or 'Sarah',
|
|
'last_name': s0.last_name or 'Ahmed',
|
|
'birth_date': s0.birth_date or '2000-05-15',
|
|
'gender': s0.gender or 'f',
|
|
'email': (s0.email or 'sarah@encoach.test'),
|
|
'application_date': fields.Date.today(),
|
|
'course_id': course.id,
|
|
'register_id': reg.id,
|
|
'state': 'submit',
|
|
})
|
|
env.cr.commit()
|
|
print(f"✓ Admission Register: {reg.name} (admissions={len(reg.admission_ids)})")
|
|
|
|
# ── 4. Exam Type + Session ──────────────────────────────────────────────
|
|
ExamType = env['op.exam.type']
|
|
ExamSession = env['op.exam.session']
|
|
Exam = env['op.exam']
|
|
ResultTpl = env['op.result.template']
|
|
|
|
et = ExamType.search([('code', '=', 'MID')], limit=1)
|
|
if not et:
|
|
et = ExamType.create({'name': 'Midterm', 'code': 'MID'})
|
|
|
|
session = ExamSession.search([('exam_code', '=', 'S26M1')], limit=1)
|
|
if not session:
|
|
session = ExamSession.create({
|
|
'name': 'Midterm Session — Spring 2026',
|
|
'course_id': course.id,
|
|
'batch_id': batch.id,
|
|
'exam_code': 'S26M1',
|
|
'start_date': '2026-04-15',
|
|
'end_date': '2026-04-20',
|
|
'exam_type': et.id,
|
|
'evaluation_type': 'normal',
|
|
'state': 'draft',
|
|
})
|
|
|
|
# 1 exam within the session so result generation can run
|
|
# Session must be in 'schedule' state for exam creation to pass the domain check
|
|
if session.state == 'draft':
|
|
session.write({'state': 'schedule'})
|
|
|
|
exam_rec = Exam.search([('exam_code', '=', 'E-S26M1-ENG')], limit=1)
|
|
if not exam_rec:
|
|
exam_rec = Exam.create({
|
|
'session_id': session.id,
|
|
'subject_id': subject.id,
|
|
'exam_code': 'E-S26M1-ENG',
|
|
'start_time': datetime(2026, 4, 15, 9, 0, 0),
|
|
'end_time': datetime(2026, 4, 15, 11, 0, 0),
|
|
'name': 'English Midterm',
|
|
'total_marks': 100,
|
|
'min_marks': 40,
|
|
})
|
|
|
|
# Result template requires ALL exams to be in 'done' state — force it.
|
|
if exam_rec.state != 'done':
|
|
exam_rec.write({'state': 'done'})
|
|
env.cr.commit()
|
|
|
|
# 1 result template (marksheet backbone)
|
|
tpl = ResultTpl.search([('exam_session_id', '=', session.id)], limit=1)
|
|
if not tpl:
|
|
tpl = ResultTpl.create({
|
|
'exam_session_id': session.id,
|
|
'name': f'Result Template — {session.name}',
|
|
'result_date': fields.Date.today(),
|
|
})
|
|
env.cr.commit()
|
|
print(f"✓ Exam session: {session.name}, result template: {tpl.name}")
|
|
|
|
# ── 5. Facility ──────────────────────────────────────────────────────────
|
|
OpFacility = env['op.facility']
|
|
fac_data = [
|
|
{'name': 'Room A101', 'code': 'A101'},
|
|
{'name': 'Lab B202', 'code': 'B202'},
|
|
{'name': 'Library', 'code': 'LIB'},
|
|
]
|
|
for f in fac_data:
|
|
if not OpFacility.search([('code', '=', f['code'])], limit=1):
|
|
OpFacility.create(f)
|
|
env.cr.commit()
|
|
print(f"✓ Facilities: {OpFacility.search_count([])}")
|
|
|
|
# ── 6. Activity type + Activity ──────────────────────────────────────────
|
|
OpActivityType = env['op.activity.type']
|
|
OpActivity = env['op.activity']
|
|
for n in ['Workshop', 'Field Trip', 'Guest Lecture']:
|
|
if not OpActivityType.search([('name', '=', n)], limit=1):
|
|
OpActivityType.create({'name': n})
|
|
workshop = OpActivityType.search([('name', '=', 'Workshop')], limit=1)
|
|
if students and workshop and not OpActivity.search([('student_id', '=', students[0].id)], limit=1):
|
|
OpActivity.create({
|
|
'student_id': students[0].id,
|
|
'faculty_id': faculty.id if faculty else False,
|
|
'type_id': workshop.id,
|
|
'description': 'Participated in IELTS Writing workshop',
|
|
'date': fields.Date.today(),
|
|
})
|
|
env.cr.commit()
|
|
print(f"✓ Activities: types={OpActivityType.search_count([])}, records={OpActivity.search_count([])}")
|
|
|
|
# ── 7. Library: Author, Publisher, Media ─────────────────────────────────
|
|
OpAuthor = env['op.author']
|
|
OpPublisher = env['op.publisher']
|
|
OpMedia = env['op.media']
|
|
|
|
authors = {}
|
|
for a in ['Jane Smith', 'Mark Brown', 'Priya Patel']:
|
|
rec = OpAuthor.search([('name', '=', a)], limit=1) or OpAuthor.create({'name': a})
|
|
authors[a] = rec
|
|
pub = OpPublisher.search([('name', '=', 'Cambridge University Press')], limit=1) \
|
|
or OpPublisher.create({'name': 'Cambridge University Press'})
|
|
|
|
media_data = [
|
|
{'name': 'IELTS Reading Practice Book', 'isbn': 'ISBN-IELTS-RD-01', 'author_names': ['Jane Smith']},
|
|
{'name': 'Academic Writing Essentials', 'isbn': 'ISBN-AWE-02', 'author_names': ['Mark Brown', 'Priya Patel']},
|
|
{'name': 'Speaking Strategies', 'isbn': 'ISBN-SS-03', 'author_names': ['Priya Patel']},
|
|
]
|
|
for m in media_data:
|
|
if OpMedia.search([('isbn', '=', m['isbn'])], limit=1):
|
|
continue
|
|
OpMedia.create({
|
|
'name': m['name'],
|
|
'isbn': m['isbn'],
|
|
'author_ids': [(6, 0, [authors[a].id for a in m['author_names']])],
|
|
'publisher_ids': [(6, 0, [pub.id])],
|
|
})
|
|
env.cr.commit()
|
|
print(f"✓ Library: authors={OpAuthor.search_count([])}, media={OpMedia.search_count([])}")
|
|
|
|
# ── 8. Lessons (encoach.lesson) ──────────────────────────────────────────
|
|
Lesson = env['encoach.lesson']
|
|
lesson_samples = [
|
|
{'name': 'Week 1 — Reading Skim & Scan', 'lesson_topic': 'Skim & Scan', 'start': datetime(2026, 3, 2, 9, 0), 'end': datetime(2026, 3, 2, 10, 30), 'status': 'scheduled'},
|
|
{'name': 'Week 1 — Writing Task 1', 'lesson_topic': 'Task 1 Intro', 'start': datetime(2026, 3, 3, 11, 0), 'end': datetime(2026, 3, 3, 12, 30), 'status': 'scheduled'},
|
|
{'name': 'Week 2 — Listening Part 3', 'lesson_topic': 'Academic Q&A', 'start': datetime(2026, 3, 9, 9, 0), 'end': datetime(2026, 3, 9, 10, 30), 'status': 'done'},
|
|
]
|
|
for ls in lesson_samples:
|
|
if Lesson.search([('name', '=', ls['name'])], limit=1):
|
|
continue
|
|
Lesson.create({
|
|
'name': ls['name'],
|
|
'lesson_topic': ls['lesson_topic'],
|
|
'course_id': course.id,
|
|
'batch_id': batch.id,
|
|
'subject_id': subject.id,
|
|
'faculty_id': faculty.id if faculty else False,
|
|
'start_datetime': ls['start'],
|
|
'end_datetime': ls['end'],
|
|
'status': ls['status'],
|
|
})
|
|
env.cr.commit()
|
|
print(f"✓ Lessons: {Lesson.search_count([])}")
|
|
|
|
# ── 9. Gradebook + lines ─────────────────────────────────────────────────
|
|
Gradebook = env['encoach.gradebook']
|
|
GradebookLine = env['encoach.gradebook.line']
|
|
for s in students:
|
|
gb = Gradebook.search([('student_id', '=', s.id), ('course_id', '=', course.id)], limit=1)
|
|
if not gb:
|
|
gb = Gradebook.create({
|
|
'student_id': s.id,
|
|
'course_id': course.id,
|
|
'academic_year_id': year.id,
|
|
})
|
|
if len(gb.line_ids) < 3:
|
|
lines = [
|
|
{'assignment_name': 'Writing Task 1', 'marks': 7.0, 'percentage': 70.0, 'state': 'graded'},
|
|
{'assignment_name': 'Reading Quiz 1', 'marks': 8.5, 'percentage': 85.0, 'state': 'graded'},
|
|
{'assignment_name': 'Speaking Part 2', 'marks': 6.0, 'percentage': 60.0, 'state': 'submitted'},
|
|
]
|
|
for l in lines:
|
|
GradebookLine.create({**l, 'gradebook_id': gb.id})
|
|
env.cr.commit()
|
|
print(f"✓ Gradebooks: {Gradebook.search_count([])}, lines: {GradebookLine.search_count([])}")
|
|
|
|
# ── 10. Student Leave (type + request) ───────────────────────────────────
|
|
LeaveType = env['encoach.student.leave.type']
|
|
Leave = env['encoach.student.leave']
|
|
for n in ['Medical', 'Personal', 'Family Emergency']:
|
|
if not LeaveType.search([('name', '=', n)], limit=1):
|
|
LeaveType.create({'name': n})
|
|
medical = LeaveType.search([('name', '=', 'Medical')], limit=1)
|
|
if students and medical and not Leave.search([('student_id', '=', students[0].id)], limit=1):
|
|
Leave.create({
|
|
'student_id': students[0].id,
|
|
'leave_type_id': medical.id,
|
|
'start_date': '2026-04-10',
|
|
'end_date': '2026-04-12',
|
|
'description': 'Flu — attached medical certificate',
|
|
'state': 'confirm',
|
|
})
|
|
env.cr.commit()
|
|
print(f"✓ Student leaves: types={LeaveType.search_count([])}, requests={Leave.search_count([])}")
|
|
|
|
# ── 11. Fees terms + student fees ────────────────────────────────────────
|
|
FeesTerms = env['op.fees.terms']
|
|
FeesTermsLn = env['op.fees.terms.line']
|
|
StudentFees = env['op.student.fees.details']
|
|
Product = env['product.product']
|
|
|
|
# A. Fees terms WITH line_ids summing to 100%
|
|
fees_term_defs = [
|
|
{'name': 'Full Payment', 'code': 'FULL', 'lines': [(0, 100.0)]},
|
|
{'name': '2 Installments (50/50)','code': 'INST2', 'lines': [(0, 50.0), (30, 50.0)]},
|
|
{'name': '3 Installments', 'code': 'INST3', 'lines': [(0, 34.0), (30, 33.0), (60, 33.0)]},
|
|
]
|
|
for d in fees_term_defs:
|
|
if FeesTerms.search([('code', '=', d['code'])], limit=1):
|
|
continue
|
|
try:
|
|
with env.cr.savepoint():
|
|
line_cmds = [(0, 0, {'due_days': dd, 'value': v}) for (dd, v) in d['lines']]
|
|
FeesTerms.create({
|
|
'name': d['name'],
|
|
'code': d['code'],
|
|
'fees_terms': 'fixed_days',
|
|
'no_days': 30,
|
|
'day_type': 'after',
|
|
'line_ids': line_cmds,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip fees term {d['name']}: {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Fees terms: {FeesTerms.search_count([])}")
|
|
|
|
# B. Product (course fees service)
|
|
fee_product = Product.search([('name', '=', 'IELTS Preparation B2 Fees')], limit=1)
|
|
if not fee_product:
|
|
try:
|
|
fee_product = Product.create({
|
|
'name': 'IELTS Preparation B2 Fees',
|
|
'type': 'service',
|
|
'list_price': 1500.0,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip product: {e})")
|
|
|
|
# Ensure the fee product has an income account wired on the current company,
|
|
# so `get_invoice()` on op.student.fees.details does not raise UserError.
|
|
if fee_product:
|
|
try:
|
|
tmpl = fee_product.product_tmpl_id
|
|
pa = tmpl.get_product_accounts() if hasattr(tmpl, 'get_product_accounts') else {}
|
|
if not (pa or {}).get('income'):
|
|
AA = env['account.account']
|
|
acc = AA.search([
|
|
('account_type', '=', 'income'),
|
|
('company_ids', 'in', env.company.id),
|
|
], limit=1) or AA.search([('account_type', '=', 'income')], limit=1)
|
|
if acc:
|
|
tmpl.with_company(env.company).write({
|
|
'property_account_income_id': acc.id,
|
|
})
|
|
env.cr.commit()
|
|
print(f" ↳ wired income account '{acc.name}' on fee product")
|
|
except Exception as e:
|
|
print(f" (skip income-account wire: {e})")
|
|
|
|
# C. Student fees (one record per student — these drive the AdminFees page)
|
|
if fee_product:
|
|
amounts = {0: 1500.0, 1: 1200.0, 2: 2000.0}
|
|
for idx, s in enumerate(students):
|
|
if StudentFees.search([('student_id', '=', s.id), ('product_id', '=', fee_product.id)], limit=1):
|
|
continue
|
|
try:
|
|
with env.cr.savepoint():
|
|
StudentFees.create({
|
|
'student_id': s.id,
|
|
'course_id': course.id,
|
|
'batch_id': batch.id,
|
|
'product_id': fee_product.id,
|
|
'amount': amounts.get(idx, 1500.0),
|
|
'date': fields.Date.today(),
|
|
'state': 'draft',
|
|
'discount': 10.0 if idx == 1 else 0.0,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip student fee for {s.id}: {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Student fees: {StudentFees.search_count([])}")
|
|
|
|
# ── 12. Marksheet register + lines so Student Progress drill-down renders ──
|
|
OpMarkLine = env['op.marksheet.line']
|
|
OpMarkReg = env['op.marksheet.register']
|
|
|
|
mreg = OpMarkReg.search([('exam_session_id', '=', session.id)], limit=1)
|
|
if not mreg:
|
|
try:
|
|
with env.cr.savepoint():
|
|
mreg = OpMarkReg.create({
|
|
'name': f'Marksheet {session.name}',
|
|
'exam_session_id': session.id,
|
|
'result_template_id': tpl.id,
|
|
'generated_date': fields.Date.today(),
|
|
'generated_by': admin.id if admin else False,
|
|
'state': 'draft',
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip marksheet.register: {e})")
|
|
mreg = False
|
|
|
|
if mreg:
|
|
for s in students[:2]:
|
|
if not OpMarkLine.search([('marksheet_reg_id', '=', mreg.id), ('student_id', '=', s.id)], limit=1):
|
|
try:
|
|
with env.cr.savepoint():
|
|
OpMarkLine.create({
|
|
'marksheet_reg_id': mreg.id,
|
|
'student_id': s.id,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip marksheet.line: {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Marksheet fixtures: registers={OpMarkReg.search_count([])}, "
|
|
f"lines={OpMarkLine.search_count([])}")
|
|
|
|
# ── 13. Support: tickets + registration codes ────────────────────────────
|
|
Ticket = env['encoach.ticket']
|
|
Code = env['encoach.code']
|
|
|
|
ticket_defs = [
|
|
('Cannot login to mobile app', 'bug',
|
|
'Login button does nothing on iOS Safari after the 2026 release.',
|
|
'open', 'platform', 'mobile/login'),
|
|
('Add dark-mode toggle', 'feature',
|
|
'Students request a persistent dark-mode across the whole student dashboard.',
|
|
'in_progress', 'webpage', '/student/dashboard'),
|
|
('Help with exam submission', 'support',
|
|
'Layla was not sure how to flag a question during the listening section.',
|
|
'resolved', 'email', '/student/exam/2/session'),
|
|
('General feedback on UI', 'feedback',
|
|
'The new admin sidebar is cleaner, but the Platform page icon is hard to see.',
|
|
'open', 'platform', '/admin/platform'),
|
|
]
|
|
for subj, ttype, desc, status, source, ctx in ticket_defs:
|
|
if Ticket.search([('subject', '=', subj)], limit=1):
|
|
continue
|
|
try:
|
|
with env.cr.savepoint():
|
|
Ticket.create({
|
|
'subject': subj,
|
|
'description': desc,
|
|
'type': ttype,
|
|
'status': status,
|
|
'source': source,
|
|
'page_context': ctx,
|
|
'reporter_id': (admin.id if admin else env.user.id),
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip ticket '{subj}': {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Tickets: {Ticket.search_count([])}")
|
|
|
|
# Registration codes (for the Settings > Codes tab)
|
|
if Code.search_count([]) < 4:
|
|
try:
|
|
code_rows = [
|
|
{'code': 'ENCOACH-2026-A1B2', 'code_type': 'individual', 'user_type': 'student', 'max_uses': 1},
|
|
{'code': 'ENCOACH-2026-C3D4', 'code_type': 'individual', 'user_type': 'student', 'max_uses': 1, 'uses': 1},
|
|
{'code': 'BATCH-2026-E5F6', 'code_type': 'corporate', 'user_type': 'corporate','max_uses': 20},
|
|
{'code': 'BATCH-2026-G7H8', 'code_type': 'corporate', 'user_type': 'corporate','max_uses': 20},
|
|
]
|
|
for row in code_rows:
|
|
if Code.search([('code', '=', row['code'])], limit=1):
|
|
continue
|
|
with env.cr.savepoint():
|
|
Code.create({**row, 'creator_id': (admin.id if admin else env.user.id)})
|
|
except Exception as e:
|
|
print(f" (skip codes seed: {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Registration codes: {Code.search_count([])}")
|
|
|
|
# ── 14. Training library: vocabulary + grammar ───────────────────────────
|
|
Vocab = env['encoach.vocab.item']
|
|
VocabProg = env['encoach.vocab.progress']
|
|
Grammar = env['encoach.grammar.rule']
|
|
GrammarProg = env['encoach.grammar.progress']
|
|
|
|
vocab_rows = [
|
|
('Ubiquitous', 'Present, appearing, or found everywhere.',
|
|
'Smartphones have become ubiquitous in modern life.',
|
|
'C1', 'adjective', 'academic'),
|
|
('Pragmatic', 'Dealing with things sensibly and realistically.',
|
|
'Her pragmatic approach to the project saved months of work.',
|
|
'B2', 'adjective', 'academic'),
|
|
('Eloquent', 'Fluent or persuasive in speaking or writing.',
|
|
'The speaker made an eloquent argument for reform.',
|
|
'C1', 'adjective', 'academic'),
|
|
('Meticulous', 'Showing great attention to detail.',
|
|
'She kept meticulous notes of every experiment.',
|
|
'B2', 'adjective', 'academic'),
|
|
('Ambiguous', 'Open to more than one interpretation.',
|
|
'The contract wording was frustratingly ambiguous.',
|
|
'B2', 'adjective', 'academic'),
|
|
('Coherent', 'Logical and consistent.',
|
|
'Your essay needs a more coherent structure.',
|
|
'B1', 'adjective', 'academic'),
|
|
('Versatile', 'Able to adapt to many different functions.',
|
|
'A versatile tool is worth its price.',
|
|
'B2', 'adjective', 'general'),
|
|
('Concise', 'Giving a lot of information in few words.',
|
|
'Please keep your answer concise.',
|
|
'B1', 'adjective', 'general'),
|
|
('Collaborate','Work jointly on an activity or project.',
|
|
'The two teams collaborated on the redesign.',
|
|
'B1', 'verb', 'business'),
|
|
('Rationale', 'A set of reasons or a logical basis for a course of action.',
|
|
'What is the rationale behind this decision?',
|
|
'C1', 'noun', 'academic'),
|
|
]
|
|
for word, meaning, ex, level, pos, cat in vocab_rows:
|
|
if Vocab.search([('word', '=', word)], limit=1):
|
|
continue
|
|
try:
|
|
with env.cr.savepoint():
|
|
Vocab.create({
|
|
'word': word,
|
|
'meaning': meaning,
|
|
'example_sentence': ex,
|
|
'level': level,
|
|
'part_of_speech': pos,
|
|
'category': cat,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip vocab '{word}': {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Vocabulary items: {Vocab.search_count([])}")
|
|
|
|
grammar_rows = [
|
|
('Conditional Sentences (Type 2 & 3)',
|
|
'If + past simple / past perfect for hypothetical or unreal past situations.',
|
|
'If I had studied, I would have passed the exam.',
|
|
'B2', 'conditionals'),
|
|
('Passive Voice in Academic Writing',
|
|
'Using passive constructions (to be + past participle) to focus on the action rather than the actor.',
|
|
'The results were analysed using SPSS.',
|
|
'B2', 'voice'),
|
|
('Relative Clauses',
|
|
'Defining vs non-defining relative clauses introduced by who/which/that.',
|
|
'The student who sits in front of me is from Egypt.',
|
|
'B1', 'clauses'),
|
|
('Subject-Verb Agreement',
|
|
'Verbs must agree with their grammatical subject, even when the subject is complex.',
|
|
'A bag of apples is on the table.',
|
|
'B1', 'agreement'),
|
|
('Modal Verbs for Speculation',
|
|
'Must have / could have / might have + past participle express certainty or possibility about the past.',
|
|
'He must have forgotten his keys — the door is locked.',
|
|
'C1', 'modals'),
|
|
('Reported Speech',
|
|
'Tense-shift and pronoun/time reference changes when reporting someone else\u2019s words.',
|
|
'She said that she was leaving the next day.',
|
|
'B2', 'speech'),
|
|
('Articles (a/an/the)',
|
|
'When to use indefinite vs definite articles with countable and uncountable nouns.',
|
|
'I bought a book. The book is excellent.',
|
|
'A2', 'articles'),
|
|
('Present Perfect vs Past Simple',
|
|
'Present perfect for experiences or actions connected to now; past simple for finished actions at a specific past time.',
|
|
'I have been to Italy. Last summer I went to Rome.',
|
|
'B1', 'tenses'),
|
|
]
|
|
for name, desc, ex, level, cat in grammar_rows:
|
|
if Grammar.search([('name', '=', name)], limit=1):
|
|
continue
|
|
try:
|
|
with env.cr.savepoint():
|
|
Grammar.create({
|
|
'name': name,
|
|
'description': desc,
|
|
'example': ex,
|
|
'level': level,
|
|
'category': cat,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip grammar '{name}': {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Grammar rules: {Grammar.search_count([])}")
|
|
|
|
# Sample progress for admin user so UI shows non-zero state.
|
|
admin_id = admin.id if admin else env.user.id
|
|
for word in ('Ubiquitous', 'Pragmatic', 'Versatile'):
|
|
v = Vocab.search([('word', '=', word)], limit=1)
|
|
if v and not VocabProg.search([('user_id', '=', admin_id), ('vocab_id', '=', v.id)], limit=1):
|
|
try:
|
|
with env.cr.savepoint():
|
|
VocabProg.create({
|
|
'user_id': admin_id, 'vocab_id': v.id,
|
|
'completed': True, 'mastery': 'mastered',
|
|
'last_reviewed': fields.Datetime.now(),
|
|
'review_count': 3,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip vocab progress '{word}': {e})")
|
|
for rule_name in ('Conditional Sentences (Type 2 & 3)',
|
|
'Relative Clauses', 'Reported Speech'):
|
|
r = Grammar.search([('name', '=', rule_name)], limit=1)
|
|
if r and not GrammarProg.search([('user_id', '=', admin_id), ('rule_id', '=', r.id)], limit=1):
|
|
try:
|
|
with env.cr.savepoint():
|
|
GrammarProg.create({
|
|
'user_id': admin_id, 'rule_id': r.id,
|
|
'completed': True,
|
|
'last_reviewed': fields.Datetime.now(),
|
|
'review_count': 2,
|
|
})
|
|
except Exception as e:
|
|
print(f" (skip grammar progress '{rule_name}': {e})")
|
|
env.cr.commit()
|
|
print(f"✓ Training progress rows: vocab={VocabProg.search_count([])}, grammar={GrammarProg.search_count([])}")
|
|
|
|
# ── Done ─────────────────────────────────────────────────────────────────
|
|
print("\n" + "=" * 60)
|
|
print(" INSTITUTIONAL FIXTURES SEEDED!")
|
|
print("=" * 60)
|
|
print(f" Academic Year: {year.name} ({len(year.academic_term_ids)} terms)")
|
|
print(f" Departments: {OpDept.search_count([])}")
|
|
print(f" Admission registers: {AdmReg.search_count([])}, admissions: {Adm.search_count([])}")
|
|
print(f" Exam sessions: {ExamSession.search_count([])}, result templates: {ResultTpl.search_count([])}")
|
|
print(f" Facilities: {OpFacility.search_count([])}")
|
|
print(f" Activities: types={OpActivityType.search_count([])}, records={OpActivity.search_count([])}")
|
|
print(f" Library media: {OpMedia.search_count([])}")
|
|
print(f" Lessons: {Lesson.search_count([])}")
|
|
print(f" Gradebooks: {Gradebook.search_count([])}")
|
|
print(f" Student leaves: {Leave.search_count([])}")
|
|
print(f" Tickets: {Ticket.search_count([])}")
|
|
print(f" Registration codes: {Code.search_count([])}")
|
|
print(f" Vocabulary items: {Vocab.search_count([])}")
|
|
print(f" Grammar rules: {Grammar.search_count([])}")
|