Files
Yamen Ahmad 6ec68160c8 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
2026-04-19 03:13:23 +04:00

158 lines
5.9 KiB
Python

"""Training library: vocabulary items, grammar rules and per-user progress.
Backs the `/admin/training/vocabulary` and `/admin/training/grammar` pages
(previously hard-coded mocks).
"""
from odoo import api, fields, models
CEFR_LEVELS = [
('A1', 'A1'),
('A2', 'A2'),
('B1', 'B1'),
('B2', 'B2'),
('C1', 'C1'),
('C2', 'C2'),
]
class EncoachVocabItem(models.Model):
_name = 'encoach.vocab.item'
_description = 'Training Vocabulary Item'
_order = 'level,word'
word = fields.Char('Word', required=True, index=True)
meaning = fields.Text('Meaning', required=True)
example_sentence = fields.Text('Example Sentence')
level = fields.Selection(CEFR_LEVELS, string='CEFR Level', default='B1', required=True)
part_of_speech = fields.Selection([
('noun', 'Noun'),
('verb', 'Verb'),
('adjective', 'Adjective'),
('adverb', 'Adverb'),
('phrase', 'Phrase'),
('other', 'Other'),
], string='Part of Speech', default='noun')
category = fields.Char('Category', default='general',
help='Tag such as "academic", "business", "daily".')
active = fields.Boolean('Active', default=True)
progress_ids = fields.One2many('encoach.vocab.progress', 'vocab_id', 'Progress')
completion_count = fields.Integer('Completions', compute='_compute_stats')
learners_count = fields.Integer('Learners', compute='_compute_stats')
@api.depends('progress_ids.completed')
def _compute_stats(self):
for rec in self:
rec.learners_count = len(rec.progress_ids)
rec.completion_count = len(rec.progress_ids.filtered(lambda p: p.completed))
@api.constrains('word')
def _check_word_unique(self):
for rec in self:
if not rec.word:
continue
dup = self.sudo().search([
('id', '!=', rec.id),
('word', '=ilike', rec.word.strip()),
], limit=1)
if dup:
from odoo.exceptions import ValidationError
raise ValidationError(f"Word '{rec.word}' already exists.")
class EncoachVocabProgress(models.Model):
_name = 'encoach.vocab.progress'
_description = 'Vocabulary Progress'
_order = 'last_reviewed desc'
user_id = fields.Many2one('res.users', required=True,
default=lambda self: self.env.user.id,
ondelete='cascade')
vocab_id = fields.Many2one('encoach.vocab.item', required=True, ondelete='cascade')
completed = fields.Boolean('Completed', default=False)
mastery = fields.Selection([
('learning', 'Learning'),
('familiar', 'Familiar'),
('mastered', 'Mastered'),
], default='learning')
last_reviewed = fields.Datetime('Last Reviewed')
review_count = fields.Integer('Review Count', default=0)
@api.constrains('user_id', 'vocab_id')
def _check_unique_user_vocab(self):
for rec in self:
dup = self.sudo().search([
('id', '!=', rec.id),
('user_id', '=', rec.user_id.id),
('vocab_id', '=', rec.vocab_id.id),
], limit=1)
if dup:
from odoo.exceptions import ValidationError
raise ValidationError("Progress already exists for this user + vocabulary item.")
class EncoachGrammarRule(models.Model):
_name = 'encoach.grammar.rule'
_description = 'Training Grammar Rule'
_order = 'level,name'
name = fields.Char('Rule', required=True, index=True)
description = fields.Text('Description', required=True)
example = fields.Text('Example')
level = fields.Selection(CEFR_LEVELS, string='CEFR Level', default='B1', required=True)
category = fields.Char('Category', default='general',
help='Tag such as "tenses", "modals", "clauses".')
active = fields.Boolean('Active', default=True)
progress_ids = fields.One2many('encoach.grammar.progress', 'rule_id', 'Progress')
completion_count = fields.Integer('Completions', compute='_compute_stats')
learners_count = fields.Integer('Learners', compute='_compute_stats')
@api.depends('progress_ids.completed')
def _compute_stats(self):
for rec in self:
rec.learners_count = len(rec.progress_ids)
rec.completion_count = len(rec.progress_ids.filtered(lambda p: p.completed))
@api.constrains('name')
def _check_rule_name_unique(self):
for rec in self:
if not rec.name:
continue
dup = self.sudo().search([
('id', '!=', rec.id),
('name', '=ilike', rec.name.strip()),
], limit=1)
if dup:
from odoo.exceptions import ValidationError
raise ValidationError(f"Grammar rule '{rec.name}' already exists.")
class EncoachGrammarProgress(models.Model):
_name = 'encoach.grammar.progress'
_description = 'Grammar Rule Progress'
_order = 'last_reviewed desc'
user_id = fields.Many2one('res.users', required=True,
default=lambda self: self.env.user.id,
ondelete='cascade')
rule_id = fields.Many2one('encoach.grammar.rule', required=True, ondelete='cascade')
completed = fields.Boolean('Completed', default=False)
last_reviewed = fields.Datetime('Last Reviewed')
review_count = fields.Integer('Review Count', default=0)
@api.constrains('user_id', 'rule_id')
def _check_unique_user_rule(self):
for rec in self:
dup = self.sudo().search([
('id', '!=', rec.id),
('user_id', '=', rec.user_id.id),
('rule_id', '=', rec.rule_id.id),
], limit=1)
if dup:
from odoo.exceptions import ValidationError
raise ValidationError("Progress already exists for this user + grammar rule.")