feat: institutional + support + training admin sections (backend + frontend)

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
This commit is contained in:
Yamen Ahmad
2026-04-19 03:13:23 +04:00
parent 50f58dc995
commit 98b9837a54
141 changed files with 20235 additions and 1453 deletions

630
seed_institutional.py Normal file
View File

@@ -0,0 +1,630 @@
#!/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([])}")