Files
full_encoach_platform/docs/PROJECT_SUMMARY.md
Yamen Ahmad 98b9837a54 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

1073 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# EnCoach Platform — Project Summary
> Last updated: 2026-04-18 | **Main branch: `v4`**
> **Latest QA passes:**
> - **2026-04-18 (training section):** Built the Training section end-to-end — the `Vocabulary` and `Grammar` pages (previously pure hard-coded mocks) are now wired to real Odoo data with CRUD, search, level filtering, and per-user progress tracking. New `encoach.vocab.item` / `encoach.vocab.progress` / `encoach.grammar.rule` / `encoach.grammar.progress` models + `/api/training/vocabulary` and `/api/training/grammar` controllers. 26/26 API write-flow actions passing (`test_training_flows.py`); both pages verified live in-browser. See §19.
> - **2026-04-18 (support section):** Built the Support section end-to-end — the `Tickets`, `Payment Record`, and `Settings` pages are now wired to real Odoo data (they were previously mocks/404s). New `encoach.ticket` model + full CRUD controller, `/api/payment-records` deriving from `op.student.fees.details` + `account.move`, `/api/codes` + `/api/packages` + `/api/grading-config` powering the Settings tabs. 29/29 API write-flow actions passing (`test_support_flows.py`); all 3 pages verified live in-browser. See §18.
> - **2026-04-18 (institutional pages write-flow):** API-level write-flow sweep across all 14 institutional admin pages — **44/44 actions passing** (CREATE / EDIT / DELETE / WORKFLOW). Numerous backend gaps fixed: missing DELETE routes added (admission-registers, admissions, result-templates, student-leaves), lesson/library detail endpoints added, custom `encoach.asset` model introduced to replace the missing `op.asset`, marksheet generate endpoint aliased for Odoo 19's `generate_result`, fees `create-invoice` auto-wires the product income account via Odoo 19's JSONB property storage, academic-year DELETE now cascades dependent terms. See §17.
> - **2026-04-18 (browser E2E):** Full end-to-end test across admin, teacher, student user types. All critical pages verified working in browser. Two backend bugs fixed: (1) `/api/approval-workflows` returned 500 because raw SQL referenced a table that doesn't exist — now gracefully returns empty list; (2) `DELETE /api/courses/<id>` returned opaque 500 when course had enrollments — now returns 409 with a clear message and supports `?force=true` for cascade-delete. See §16.
---
## 1. Project Overview
EnCoach is an AI-powered online learning and examination platform built on **Odoo 19** (backend) and **React + TypeScript + Vite** (frontend). It supports IELTS and general English exam generation, student exam sessions, auto-scoring, adaptive learning, and course creation.
---
## 2. Tech Stack
| Layer | Technology |
|-------|-----------|
| **Backend** | Odoo 19, Python 3.12, PostgreSQL 18 |
| **Frontend** | React 18, TypeScript, Vite 5, Tailwind CSS, Shadcn UI |
| **State Management** | `@tanstack/react-query` (mutations + queries) |
| **Auth** | JWT (issued by Odoo, validated via `validate_token` helper) |
| **AI** | OpenAI API (with fallback templates when API key not configured) |
| **Database** | `encoach_v2` (PostgreSQL 18.3) |
---
## 3. User Credentials
Admin/teacher users use password **`admin`**. Student passwords were reset to **`student123`** during end-to-end testing.
| ID | Login | Name | Type | Password |
|----|-------|------|------|----------|
| 2 | `admin` | Administrator | superadmin | `admin` |
| 5 | `admin@encoach.test` | Admin User | admin | `admin` |
| 6 | `sarah@encoach.test` | Sarah Ahmed | student | **`student123`** |
| 7 | `omar@encoach.test` | Omar Khan | student | **`student123`** |
| 8 | `layla@encoach.test` | Layla Nasser | student | **`student123`** |
| 9 | `khalid@encoach.test` | Dr. Khalid | teacher | `admin` |
| 10 | `fatima@encoach.test` | Ms. Fatima | teacher | `admin` |
> **Note:** Student passwords were bulk-reset to `student123` via a direct `psycopg2` + `passlib` script during testing. If a student can't login, reset their password in psql using `passlib.context.CryptContext(['pbkdf2_sha512'])`.
### Login API
```bash
# Admin login
curl -X POST http://localhost:8069/api/login \
-H "Content-Type: application/json" \
-d '{"email":"admin","password":"admin"}'
# Student login
curl -X POST http://localhost:8069/api/login \
-H "Content-Type: application/json" \
-d '{"email":"sarah@encoach.test","password":"student123"}'
```
Returns `{ token, user, permissions }`. The `token` (JWT) is used as `Authorization: Bearer <token>` for all subsequent API calls.
---
## 4. How to Start the Project
### Prerequisites
- PostgreSQL 18 binary at `/Users/yamenahmad/micromamba/envs/odoo19/bin/`
- Python 3.12 (conda env at `.conda-envs/odoo19/` or micromamba)
- Node.js 20 at `/Users/yamenahmad/.local/node/bin/`
### Start PostgreSQL
```bash
PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
PGDATA="/Users/yamenahmad/projects2026/odoo/odoo19/pgdata"
"$PGBIN/pg_ctl" -D "$PGDATA" -l "$PGDATA/logfile" start
```
> **Important:** The pgdata was initialized with PG 18. The conda env has PG 17.6, which is **incompatible**. Always use the micromamba PG 18 binary above.
>
> If PG fails with config errors, check `pgdata/logfile`. The parameter `autovacuum_worker_slots` (PG 18-only) was commented out in `pgdata/postgresql.conf` line 687 for compatibility.
### Start Odoo Backend
```bash
cd /Users/yamenahmad/projects2026/odoo/odoo19
./run.sh
```
Runs at **http://127.0.0.1:8069**
> **Odoo config** (`odoo.conf`) must have `db_name = encoach_v2` and `dbfilter = ^encoach_v2$`, otherwise API routes return 404 ("No database selected").
### Start Frontend
```bash
export PATH="/Users/yamenahmad/.local/node/bin:$PATH"
cd /Users/yamenahmad/projects2026/odoo/odoo19/frontend
npm run dev
```
Runs at **http://localhost:8080** — Vite proxies `/api` requests to Odoo `:8069`.
### Stop PostgreSQL
```bash
"$PGBIN/pg_ctl" -D "$PGDATA" stop
```
---
## 5. Project Structure
```
odoo19/
├── backend/
│ ├── custom_addons/ # All EnCoach Odoo modules
│ │ ├── encoach_api/ # Auth (login/logout/user) + base helpers
│ │ ├── encoach_ai/ # AI generation, exam generation/submit
│ │ ├── encoach_ai_course/ # AI course creation pipelines
│ │ ├── encoach_adaptive/ # Adaptive learning engine
│ │ ├── encoach_core/ # Core models (user, entity, role, permission)
│ │ ├── encoach_exam_template/ # Exam CRUD, sessions, rubrics, assignments, schedules
│ │ ├── encoach_scoring/ # Grading queue, score release
│ │ ├── encoach_taxonomy/ # Subject/topic tree
│ │ ├── encoach_lms_api/ # LMS: courses, chapters, materials, progress, enrollment
│ │ ├── encoach_course_gen/ # Course generation
│ │ ├── encoach_branding/ # Entity branding
│ │ ├── encoach_signup/ # Registration, email verification, onboarding
│ │ ├── encoach_placement/ # Placement test
│ │ └── ...
│ └── openeducat_erp-19.0/ # OpenEduCat base modules
├── frontend/
│ ├── src/
│ │ ├── pages/ # Route pages (admin/, student/, etc.)
│ │ ├── components/ # Reusable UI components
│ │ ├── services/ # API service classes
│ │ ├── hooks/ # React Query hooks
│ │ ├── lib/ # api-client, utils
│ │ └── types/ # TypeScript type definitions
│ ├── .env.development # VITE_API_BASE_URL=/api
│ └── .env.production # VITE_API_BASE_URL=https://api.encoach.com/api
├── docs/ # Documentation
├── odoo/ # Odoo 19 source code
├── odoo.conf # Odoo configuration
├── pgdata/ # PostgreSQL data directory (PG 18)
├── run.sh # Start Odoo
├── start.sh # Start PG + Odoo
└── stop.sh # Stop services
```
---
## 6. Git Remotes & Repositories
> **The `v4` branch is the main working branch.** All active development happens here.
| Remote | URL | Purpose |
|--------|-----|---------|
| `origin` | `https://git.albousalh.com/devops/encoach_backend_new_v2.git` | v2 backend (full monorepo) |
| `backend-v3` | `https://git.albousalh.com/devops/encoach_backend_new_v3.git` | v3 monorepo push |
| `frontend-v3` | `https://git.albousalh.com/devops/encoach_frontend_new_v3.git` | v3 monorepo push |
| `backend-v4` | `https://git.albousalh.com/devops/encoach_backend_v4.git` | v4 backend only (subtree) |
| `frontend-v4` | `https://git.albousalh.com/devops/encoach_frontend_v4.git` | v4 frontend only (subtree) |
### Pushing to v3 (full monorepo to both)
```bash
git push backend-v3 v4:main
git push frontend-v3 v4:main
```
### Pushing to v4 (subtree — only the subfolder)
```bash
git subtree push --prefix=backend backend-v4 main
git subtree push --prefix=frontend frontend-v4 main
```
### Git Credentials
Stored in macOS Keychain for `git.albousalh.com`:
- Username: `yamen`
- Auth: token-based (stored in keychain)
---
## 7. API Routes Reference
### Authentication (`encoach_api`)
| Method | Route | Auth | Description |
|--------|-------|------|-------------|
| POST | `/api/login` | public | Login, returns JWT |
| GET | `/api/user` | JWT | Get current user |
| POST | `/api/logout` | public | Logout (client clears token) |
### Exam Generation & AI (`encoach_ai`)
| Method | Route | Description |
|--------|-------|-------------|
| POST | `/api/exam/<module>/generate` | AI-generate content for a module (reading/listening/writing/speaking) |
| POST | `/api/exam/generation/submit` | Submit generated exam, create questions |
| POST | `/api/exam/<module>/generate/save` | Save generated content |
### Exam Session (`encoach_exam_template`)
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/api/exam/<exam_id>/session` | Get exam session (creates attempt if needed) |
| POST | `/api/exam/<exam_id>/autosave` | Autosave student answers |
| POST | `/api/exam/<exam_id>/submit` | Submit exam for scoring |
| GET | `/api/exam/<exam_id>/status` | Get attempt status |
| GET | `/api/exam/<exam_id>/results` | Get exam results |
### Custom Exams
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/api/exam/custom/list` | List custom exams |
| POST | `/api/exam/custom/create` | Create custom exam |
| GET/PUT/DELETE | `/api/exam/custom/<id>` | CRUD on custom exam |
### Exam Structures, Templates, Schedules, Assignments
| Method | Route | Description |
|--------|-------|-------------|
| GET/POST | `/api/exam-structures` | List/create exam structures |
| GET/POST | `/api/exam/templates` | List/create exam templates |
| GET/POST | `/api/exam-schedules` | List/create schedules |
| GET | `/api/student/my-exams` | Student's assigned exams |
| GET/POST | `/api/assignments` | List/create assignments |
### Rubrics & Approval Workflows
| Method | Route | Description |
|--------|-------|-------------|
| GET/POST | `/api/rubrics` | List/create rubrics |
| GET/POST | `/api/rubric-groups` | List/create rubric groups |
| GET/POST | `/api/approval-workflows` | List/create approval workflows |
| POST | `/api/approval-requests/<id>/approve` | Approve request |
| POST | `/api/approval-requests/<id>/reject` | Reject request |
### Entities
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/api/entities` | List entities |
### LMS Core (`encoach_lms_api` — `lms_core.py`)
| Method | Route | Description |
|--------|-------|-------------|
| GET/POST | `/api/courses` | List all courses (paginated) / create course |
| GET/PUT/DELETE | `/api/courses/<id>` | Get / update / delete a single course |
| GET/POST | `/api/students` | List students / create student |
| GET/PUT/DELETE | `/api/students/<id>` | Get / update / delete a student |
| GET | `/api/student/my-courses` | Enrolled courses for logged-in student (with progress) |
| POST | `/api/students/<id>/enroll` | Enroll a student in one or more courses |
| POST | `/api/courses/<id>/enroll` | Bulk-enroll students in a course |
| GET/POST | `/api/teachers` | List / create teachers |
| GET/PUT/DELETE | `/api/teachers/<id>` | Get / update / delete a teacher |
| GET/POST | `/api/batches` | List / create batches |
| GET/PUT/DELETE | `/api/batches/<id>` | Get / update / delete a batch |
### LMS Courseware (`encoach_lms_api` — `courseware.py`)
| Method | Route | Description |
|--------|-------|-------------|
| GET/POST | `/api/courses/<id>/chapters` | List / create chapters for a course |
| GET/PUT/DELETE | `/api/chapters/<id>` | Get / update / delete a chapter |
| POST | `/api/chapters/<id>/materials` | Upload material (JSON or FormData) |
| GET/PUT/DELETE | `/api/materials/<id>` | Get / update / delete a material |
| GET | `/api/materials/<id>/download` | Download material file |
| GET | `/api/materials/<id>/content` | Serve material file inline (for iframe embed, accepts ?token= param) |
| POST | `/api/materials/<id>/view` | Mark material as viewed |
| POST | `/api/chapters/<id>/progress/complete` | Mark chapter as completed |
| GET | `/api/courses/<id>/completion` | Get course completion status (% progress, post-test availability) |
| GET | `/api/materials/search` | Search all materials across courses (for Admin Resources page) |
### Institutional (`encoach_lms_api`)
These power the 14 institutional admin pages under `/admin/*` (see §14) and back the write-flow test in §17. Every route is JWT-protected.
**Academic & Departments**
| Method | Route | Description |
|--------|-------|-------------|
| GET / POST | `/api/academic-years` | List / create academic years |
| GET / PATCH / DELETE | `/api/academic-years/<yid>` | Get / update / delete year (DELETE cascades terms) |
| POST | `/api/academic-years/<yid>/generate-terms` | Auto-create terms spanning the year (`two_sem`, `three_trim`, `four_qtr`) |
| GET / POST | `/api/academic-terms` | List / create academic terms |
| GET / PATCH / DELETE | `/api/academic-terms/<tid>` | Term CRUD |
| GET / POST | `/api/departments` | List / create departments |
| GET / PATCH / DELETE | `/api/departments/<did>` | Department CRUD |
**Admissions**
| Method | Route | Description |
|--------|-------|-------------|
| GET / POST | `/api/admission-registers` | List / create admission registers |
| GET / PATCH / DELETE | `/api/admission-registers/<rid>` | Register CRUD (DELETE supported) |
| POST | `/api/admission-registers/<rid>/confirm` | Confirm register |
| GET / POST | `/api/admissions` | List / create admissions (application_date **must** fall inside register window) |
| GET / PATCH / DELETE | `/api/admissions/<aid>` | Admission CRUD (DELETE supported) |
**Institutional Exams & Marksheets**
| Method | Route | Description |
|--------|-------|-------------|
| GET / POST | `/api/inst-exam-sessions` | List / create exam sessions |
| GET / PATCH / DELETE | `/api/inst-exam-sessions/<sid>` | Session CRUD |
| POST | `/api/inst-exam-sessions/<sid>/schedule` | Schedule session |
| GET / POST | `/api/result-templates` | List / create result templates |
| GET / PATCH / DELETE | `/api/result-templates/<tid>` | Template CRUD (DELETE cascades `op.marksheet.register`) |
| POST | `/api/result-templates/<tid>/generate` (alias: `/generate-marksheets`) | Generate marksheet registers (tries `generate_result` then `action_generate_marksheet`) |
| GET | `/api/marksheets` | List marksheet registers |
| POST | `/api/marksheets/<mid>/validate` | Validate marksheet |
**Facilities, Assets, Activities**
| Method | Route | Description |
|--------|-------|-------------|
| GET / POST | `/api/facilities` | List / create facilities |
| GET / PATCH / DELETE | `/api/facilities/<fid>` | Facility CRUD |
| GET / POST | `/api/assets` | List / create assets (backed by custom `encoach.asset` model) |
| GET / PATCH / DELETE | `/api/assets/<aid>` | Asset CRUD |
| GET / POST / PATCH / DELETE | `/api/activity-types` + `/<id>` | Activity-type CRUD |
| GET / POST / PATCH / DELETE | `/api/activities` + `/<id>` | Activity CRUD |
**Library, Lessons, Gradebook, Leaves, Fees**
| Method | Route | Description |
|--------|-------|-------------|
| GET / POST | `/api/library/media` | List / create media items (authors split into `author_ids`) |
| GET / PATCH / DELETE | `/api/library/media/<mid>` | Media CRUD — detail endpoint returns `author_names` + `author_ids` |
| GET / POST | `/api/lessons` | List / create lessons |
| GET / PATCH / DELETE | `/api/lessons/<lid>` | Lesson CRUD — detail endpoint preserves `course_id` / `batch_id` on edits |
| GET / POST / DELETE | `/api/grading-assignments` + `/<id>` | Grading assignment CRUD (auto-resolves `assignment_type` and default `faculty_id`) |
| GET | `/api/gradebooks` + `/api/gradebook-lines?gradebook_id=<id>` | Gradebook drill-down |
| GET | `/api/student-progress` + `/<id>` | Student progress list/detail |
| GET / POST | `/api/student-leaves` | List / create leave requests |
| POST | `/api/student-leaves/<lid>/approve` | Approve leave |
| DELETE | `/api/student-leaves/<lid>` | Delete leave |
| GET / POST | `/api/fees-plans` | List / create fee plans |
| POST | `/api/fees-plans/<fid>/create-invoice` | Generate accounting invoice (auto-wires income account on the product template for Odoo 19 JSONB property storage if missing) |
| POST | `/api/fees-plans/<fid>/register-payment` | Register a payment against the invoice |
### Other
| Route | Description |
|-------|-------------|
| `/api/taxonomy/*` | Subject/topic taxonomy tree |
| `/api/adaptive/*` | Adaptive learning dashboard/signals |
| `/api/placement/*` | Placement test start/answer/results |
| `/api/grading/*` | Grading queue, AI suggest |
| `/api/scores/*` | Score release/reject |
| `/api/course/*` | Course CRUD, gap analysis, auto-generate |
| `/api/ai-course/*` | AI course creation pipelines |
| `/api/auth/register` | User registration |
---
## 8. Key Odoo Models
| Model | Table | Description |
|-------|-------|-------------|
| `encoach.exam.custom` | `encoach_exam_custom` | Custom exams (16 records) |
| `encoach.exam.custom.section` | `encoach_exam_custom_section` | Exam sections (reading/listening/writing/speaking) |
| `encoach.question` | `encoach_question` | Questions (38 records) |
| `encoach.student.attempt` | `encoach_student_attempt` | Student exam attempts (10 records) |
| `encoach.student.answer` | `encoach_student_answer` | Individual answers |
| `encoach.entity` | `encoach_entity` | Organizations (1: "EnCoach Demo Academy") |
| `encoach.rubric` | `encoach_rubric` | Grading rubrics |
| `encoach.exam.template` | `encoach_exam_template` | Exam templates |
| `encoach.exam.structure` | `encoach_exam_structure` | Exam structures |
| `encoach.exam.schedule` | `encoach_exam_schedule` | Exam schedules |
| `encoach.exam.assignment` | `encoach_exam_assignment` | Exam assignments |
| `encoach.course.chapter` | `encoach_course_chapter` | Course chapters (ordered by sequence) |
| `encoach.chapter.material` | `encoach_chapter_material` | Materials within chapters (pdf, video, article, link) |
| `encoach.chapter.progress` | `encoach_chapter_progress` | Per-student chapter progress tracking |
| `encoach.course.completion` | `encoach_course_completion` | Per-student course completion (% progress, post-test flag) |
| `encoach.resource` | `encoach_resource` | Learning resources linked to taxonomy (subject, domain, topics, objectives) |
| `encoach.resource.tag` | `encoach_resource_tag` | Tags for categorizing resources |
| `encoach.subject` | `encoach_subject` | Taxonomy subjects (e.g. English, Mathematics) |
| `encoach.domain` | `encoach_domain` | Taxonomy domains under subjects (e.g. Grammar, Listening) |
| `encoach.topic` | `encoach_topic` | Taxonomy topics under domains |
| `encoach.learning.objective` | `encoach_learning_objective` | Learning objectives under topics (Bloom levels) |
| `op.course` (extended) | `op_course` | OpenEduCat course + `description`, `max_capacity`, `encoach_subject_id`, `topic_ids`, `learning_objective_ids` |
| `op.student` | `op_student` | OpenEduCat student model |
| `op.faculty` | `op_faculty` | OpenEduCat teacher/faculty model |
| `op.batch` | `op_batch` | OpenEduCat batch model |
### Institutional models (OpenEduCat + EnCoach)
| Model | Table | Description |
|-------|-------|-------------|
| `op.academic.year` | `op_academic_year` | Academic year (name unique, `term_structure`) |
| `op.academic.term` | `op_academic_term` | Term under a year; FK to year is `RESTRICT` — delete-year cascades terms first |
| `op.department` | `op_department` | Department |
| `op.admission.register` | `op_admission_register` | Admission register window (`start_date` / `end_date`) |
| `op.admission` | `op_admission` | Admission — constrained so `application_date ∈ [register.start_date, register.end_date]` |
| `op.exam.session` | `op_exam_session` | Institutional exam session |
| `op.result.template` | `op_result_template` | Marksheet result template — `generate_result()` in Odoo 19 (older: `action_generate_marksheet`) produces `op.marksheet.register` rows |
| `op.marksheet.register` | `op_marksheet_register` | Generated marksheet register (lines + validate workflow) |
| `op.facility` | `op_facility` | Physical facility (rooms, labs, etc.) |
| `encoach.asset` | `encoach_asset` | **New lightweight asset model** (replaces missing `op.asset`): `name`, `code`, `facility_id`, `description`, `active` |
| `op.activity.type` / `op.activity` | `op_activity_type` / `op_activity` | Activity categorization + activities |
| `op.media` / `op.author` | `op_media` / `op_author` | Library media + authors (M2M) |
| `encoach.lesson` | `encoach_lesson` | Lesson tied to course + batch |
| `grading.assignment` / `grading.assignment.type` | `grading_assignment*` | Grading assignments — `assignment_type` and `faculty_id` are required |
| `encoach.student.leave` | `encoach_student_leave` | Student leave requests with approve workflow |
| `op.fees.terms` + `.line` | `op_fees_terms*` | Fees terms; `line_ids` must sum to 100% |
| `op.student.fees.details` | `op_student_fees_details` | Per-student fees; `get_invoice()` requires an income account on product template (stored as JSONB in Odoo 19) |
---
## 9. Installed EnCoach Modules
| Module | Description |
|--------|-------------|
| `encoach_adaptive` | Adaptive learning engine |
| `encoach_ai` | AI generation, search, insights |
| `encoach_ai_course` | AI course creation |
| `encoach_api` | REST API auth + base helpers |
| `encoach_branding` | Entity branding |
| `encoach_core` | Core models (user, entity, role) |
| `encoach_course_gen` | Course generation |
| `encoach_exam_template` | Exam CRUD, sessions, rubrics, assignments |
| `encoach_lms_api` | LMS: courses, students, teachers, batches, chapters, materials, enrollment, progress |
| `encoach_quality_gate` | Quality gate checks |
| `encoach_resources` | Learning resources |
| `encoach_scoring` | Grading and score release |
| `encoach_taxonomy` | Subject/topic taxonomy |
| `encoach_vector` | Vector embeddings |
---
## 10. Exam Lifecycle Flow
```
1. Admin → /admin/generation page
├── Select modules (reading, listening, writing, speaking)
├── Set parameters (difficulty, timer, grading, rubric, entity, etc.)
└── Click "Generate with AI" → POST /api/exam/<module>/generate
2. AI generates content (or fallback templates if OpenAI not configured)
3. Admin reviews generated content → Submit
├── "Submit & skip approval" → POST /api/exam/generation/submit (skip_approval=true)
├── "Submit for approval" → POST /api/exam/generation/submit (skip_approval=false)
└── Creates: exam_custom + sections + questions in DB
4. Admin assigns exam → /api/exam/custom/<id>/assign or /api/exam-schedules
5. Student → /student/my-exams → clicks exam → /student/exam/<id>
├── GET /api/exam/<id>/session → loads sections, questions, passage_text
├── POST /api/exam/<id>/autosave → saves answers periodically
└── POST /api/exam/<id>/submit → auto-scores objective questions
6. Student → /student/exam/<id>/results → views score breakdown
```
---
## 11. Student Learning Lifecycle Flow
```
1. Admin/Teacher → /teacher/courses → "New Course"
├── Create course (title, code, description, max_capacity)
└── Course stored in op.course (extended with description, max_capacity)
2. Teacher → /teacher/courses/:courseId/chapters → "Add Chapter"
├── Create chapters with sequence ordering
└── Upload materials (pdf, video, article, link) per chapter
└── POST /api/chapters/<id>/materials (JSON or FormData)
3. Admin → /admin/courses → "Enroll Students"
├── Bulk-enroll students via POST /api/courses/<id>/enroll
└── Or enroll individually via POST /api/students/<id>/enroll
4. Student → /student/dashboard → sees enrolled courses with progress
├── GET /api/student/my-courses → returns courses + chapter_count + progress %
└── Cards show "Start Learning" or "Continue Learning"
5. Student → /student/courses/:id → course detail with chapter list
├── Shows chapter list with material counts and lock status
└── Student clicks a chapter → /student/courses/:courseId/chapters/:chapterId
6. Student → chapter view → views materials
├── POST /api/materials/<id>/view → marks material as viewed
├── Progress tracked in encoach.chapter.progress
└── When all materials viewed → chapter auto-completes
7. Student → clicks "Mark Chapter Complete"
├── POST /api/chapters/<id>/progress/complete
└── Triggers _check_course_completion()
8. Course completion check
├── When all chapters completed → encoach.course.completion updated
├── progress = 100%, completed_at = now
└── post_test_available = True → student can take final exam
```
---
## 12. Taxonomy ↔ Resource ↔ Course Relationships
```
encoach.subject (e.g. English)
└── encoach.domain (e.g. Grammar, Listening, Reading)
└── encoach.topic (e.g. Verb Tenses, Relative Clauses)
└── encoach.learning.objective (Bloom levels: Remember, Understand, Apply...)
encoach.resource ──M2O──→ encoach.subject (subject_id)
──M2O──→ encoach.domain (domain_id)
──M2M──→ encoach.topic (topic_ids)
──M2M──→ encoach.learning.objective (learning_objective_ids)
op.course (extended) ──M2O──→ encoach.subject (encoach_subject_id)
──M2M──→ encoach.topic (topic_ids)
──M2M──→ encoach.learning.objective (learning_objective_ids)
encoach.course.chapter ──M2O──→ encoach.subject (encoach_subject_id)
──M2M──→ encoach.topic (topic_ids)
──M2M──→ encoach.learning.objective (learning_objective_ids)
```
### Frontend: `TaxonomyCascade` Component
Reusable cascading picker (`frontend/src/components/TaxonomyCascade.tsx`) used in:
- Admin Resource Manager (upload + edit dialogs)
- Teacher Library (upload dialog)
Provides Subject → Domain → Topics → Learning Objectives selection with auto-fetching and reset-on-parent-change logic.
### API endpoints for taxonomy data
| Method | Route | Description |
|--------|-------|-------------|
| GET | `/api/subjects` | List subjects (paginated, returns `items` array) |
| GET | `/api/domains?subject_id=N` | List domains filtered by subject |
| GET | `/api/topics?domain_id=N` | List topics filtered by domain |
| GET | `/api/learning-objectives?topic_id=N` | List objectives filtered by topic |
---
## 13. Known Issues & Notes
1. **PostgreSQL version**: pgdata is PG 18; must use micromamba `pg_ctl` (`/Users/yamenahmad/micromamba/envs/odoo19/bin/pg_ctl`), NOT conda env PG 17.
2. **`odoo.conf` requires `db_name`**: Without `db_name = encoach_v2`, all API routes return 404.
3. **`npm` not in default PATH**: Must add `/Users/yamenahmad/.local/node/bin` to PATH before running frontend.
4. **OpenAI not configured**: AI generation uses fallback templates (static mock content). Set OpenAI API key in Odoo system parameters to enable real AI.
5. **Gitea deployment hook**: Frontend v3 server has a `docker-compose.yml` conflict that blocks auto-deploy. SSH and stash/checkout on server to fix.
6. **Student passwords were reset**: Student demo users (`sarah`, `omar`, `layla`) now use password **`student123`** (not `admin`). Passwords were reset via `psycopg2` + `passlib` script. To reset again:
```python
from passlib.context import CryptContext
ctx = CryptContext(schemes=['pbkdf2_sha512'])
new_hash = ctx.hash('student123')
# UPDATE res_users SET password = '<new_hash>' WHERE login = 'sarah@encoach.test';
```
7. **`op.course` extended fields**: The `op.course` model (OpenEduCat) was extended via `encoach_lms_api/models/course_ext.py` to add `description` (Text) and `max_capacity` (Integer). If the module is not installed/updated, course creation will fail with `ValueError: Invalid field 'description'`.
8. **`material_count` computed field**: The `material_count` on `encoach.course.chapter` requires `@api.depends('material_ids')` to recompute when materials are added. If counts show 0 after adding materials, restart Odoo with `-u encoach_lms_api`.
9. **Taxonomy API response format**: Taxonomy list endpoints (`/api/subjects`, `/api/domains`, `/api/topics`) return paginated responses with data under an `items` key (`{"items": [...], "total": N}`), not a `data` key. The frontend `taxonomy.service.ts` handles both formats.
10. **Resource taxonomy fields via FormData**: When uploading resources, `topic_ids` and `learning_objective_ids` are sent as comma-separated strings in multipart FormData. The backend `create_resource` handler parses these into proper Many2many commands `[(6, 0, ids)]`.
11. **Module upgrades for schema changes**: After adding new fields to Odoo models (e.g. `encoach_subject_id` on `op.course`), you must run `./run.sh -u <module_name> --stop-after-init` to apply DB schema changes before the server can use them.
---
## 14. Frontend Key Pages
### Admin Pages
| Route | Component | Description |
|-------|-----------|-------------|
| `/admin/dashboard` | AdminLmsDashboard | Platform overview, AI tips, stats (LMS-focused) |
| `/admin/platform` | AdminDashboard | Alternative platform dashboard |
| `/admin/courses` | AdminCourses | Course table with enroll/assign/delete actions |
| `/admin/students` | AdminStudents | Student management |
| `/admin/teachers` | AdminTeachers | Teacher management |
| `/admin/users` | UsersPage | All platform users + roles (Management section) |
| `/admin/entities` | EntitiesPage | Entity/organization management |
| `/admin/classrooms` | ClassroomsPage | Batches/classrooms with student management |
| `/admin/user-roles` | UserRoles | User role assignments (⚠ NOT `/admin/roles`) |
| `/admin/roles-permissions` | RolesPermissions | Roles & permissions matrix |
| `/admin/authority-matrix` | AuthorityMatrix | Authority matrix across roles |
| `/admin/generation` | GenerationPage | AI exam generation |
| `/admin/examsList` | ExamsListPage | List all exams (⚠ NOT `/admin/exams`) |
| `/admin/rubrics` | RubricsPage | Manage rubrics |
| `/admin/assignments` | AssignmentsPage | Manage assignments |
| `/admin/approval-workflows` | ApprovalWorkflowsPage | Approval workflow config |
| `/admin/taxonomy` | TaxonomyManager | Manage subjects, domains, topics, objectives (inline editable) |
| `/admin/resources` | ResourceManager | Upload/edit resources with full taxonomy cascade (Subject→Domain→Topics→Objectives) |
### Admin — Institutional Pages
Backed by `encoach_lms_api` + OpenEduCat models. All 14 pages verified CREATE / EDIT / DELETE / WORKFLOW in the write-flow sweep (see §17).
| Route | Page | Backing API | Key actions |
|-------|------|-------------|-------------|
| `/admin/academic-years` | Academic Years | `/api/academic-years`, `/generate-terms` | CREATE, generate-terms, EDIT, DELETE (cascades terms) |
| `/admin/academic-terms` | Academic Terms | `/api/academic-terms` | CRUD |
| `/admin/departments` | Departments | `/api/departments` | CRUD |
| `/admin/admission-registers` | Admission Registers | `/api/admission-registers` | CREATE, confirm, DELETE |
| `/admin/admissions` | Admissions | `/api/admissions` | CREATE (date-constrained), DELETE |
| `/admin/inst-exam-sessions` | Institutional Exam Sessions | `/api/inst-exam-sessions` | CREATE, schedule, EDIT, DELETE |
| `/admin/marksheets` | Marksheets | `/api/result-templates`, `/api/marksheets` | Create template, generate, validate marksheet, DELETE |
| `/admin/facilities` | Facilities | `/api/facilities`, `/api/assets` | Facility CRUD + nested assets |
| `/admin/activities` | Activities | `/api/activity-types`, `/api/activities` | Type + activity CRUD |
| `/admin/library` | Library | `/api/library/media` | Media + authors, detail exposes author links |
| `/admin/lessons` | Lessons | `/api/lessons` | CRUD; edits preserve `course_id` / `batch_id` |
| `/admin/gradebook` | Gradebook | `/api/grading-assignments`, `/api/gradebooks`, `/api/gradebook-lines` | Assignment CRUD + gradebook drill |
| `/admin/student-progress` | Student Progress | `/api/student-progress` | Drill-through detail |
| `/admin/student-leave` | Student Leave | `/api/student-leaves` | CREATE, approve, DELETE |
| `/admin/fees` | Fees | `/api/fees-plans`, `/create-invoice`, `/register-payment` | List seeded plans, generate invoice, register payment |
### Teacher Pages
| Route | Component | Description |
|-------|-----------|-------------|
| `/teacher/dashboard` | TeacherDashboard | Teacher overview |
| `/teacher/courses` | TeacherCourses | Course cards with Edit, Chapters & Materials, AI Workbench |
| `/teacher/courses/new` | CourseBuilder | Create new course |
| `/teacher/courses/:courseId/chapters` | CourseChapters | Manage chapters and materials for a course |
| `/teacher/courses/:courseId/chapters/:chapterId` | ChapterDetail | Chapter detail with material list |
| `/teacher/courses/:courseId/workbench` | AiWorkbench | AI content generation workbench |
| `/teacher/assignments` | TeacherAssignments | Manage assignments |
| `/teacher/library` | TeacherLibrary | Upload resources with full taxonomy cascade (Subject→Domain→Topics→Objectives) |
### Student Pages
| Route | Component | Description |
|-------|-----------|-------------|
| `/student/dashboard` | StudentDashboard | Enrolled courses, progress, grades, quick actions |
| `/student/courses` | StudentCourses | Enrolled courses with progress badges |
| `/student/courses/:id` | StudentCourseDetail | Course chapters, materials, grades |
| `/student/courses/:courseId/chapters/:chapterId` | StudentChapterView | View materials, mark viewed/complete |
| `/student/my-exams` | StudentExamsPage | Student's assigned exams |
| `/student/exam/:id` | ExamSession | Take an exam |
| `/student/exam/:id/results` | ExamResults | View results |
| `/student/assignments` | StudentAssignments | Homework assignments |
---
## 15. Database Connection
```
Host: localhost
Port: 5432
Database: encoach_v2
User: yamenahmad
Password: (none)
```
Quick access:
```bash
PGBIN="/Users/yamenahmad/micromamba/envs/odoo19/bin"
"$PGBIN/psql" -U yamenahmad -d encoach_v2
```
---
## 16. QA Test Report — 2026-04-18
A full end-to-end browser test was executed across all three user types (admin, teacher, student), driven by an autonomous browser agent. 32 distinct pages were verified plus an exam session flow.
### 16.1 Backend API Health — 32/33 endpoints healthy (pre-fix)
All tested with admin JWT unless noted:
| Category | Endpoints Tested | Status |
|----------|-----------------|--------|
| Auth | `/api/login`, `/api/user` | ✅ |
| Users & Roles | `/api/users/list`, `/api/users/with-roles`, `/api/roles`, `/api/permissions`, `/api/authority-matrix`, `/api/user-authority-matrix` | ✅ |
| Entities | `/api/entities`, `/api/entities/<id>/roles` | ✅ |
| LMS Core | `/api/courses`, `/api/students`, `/api/teachers`, `/api/batches`, `/api/batches/<id>/students` | ✅ |
| Taxonomy | `/api/subjects`, `/api/domains`, `/api/topics`, `/api/learning-objectives` | ✅ |
| Resources | `/api/resources`, `/api/resource-tags` | ✅ |
| Exams | `/api/exam/custom/list`, `/api/exam/templates`, `/api/exam-structures`, `/api/exam-schedules`, `/api/rubrics`, `/api/rubric-groups` | ✅ |
| Assignments | `/api/assignments` | ✅ |
| Stats | `/api/stats`, `/api/stats/performance`, `/api/analytics/student`, `/api/adaptive/dashboard` | ✅ |
| Courseware | `/api/courses/<id>/chapters`, `/api/courses/<id>/completion`, `/api/materials/search` | ✅ |
| Grading | `/api/grading/queue` | ✅ |
| Student role | `/api/student/my-courses`, `/api/student/my-exams`, `/api/assignments` | ✅ |
| Approvals | `/api/approval-workflows`, `/api/approval-requests`, `/api/approval-users` | ✅ (after fix) |
### 16.2 Bugs Fixed During This Pass
**Bug #1: `/api/approval-workflows` returned 500**
- **Cause**: `encoach_exam_template/controllers/approval_workflows.py` ran raw SQL against tables (`encoach_approval_workflow`, `encoach_approval_request`) that are only created by the `encoach_approval` module — which lives in `new_project/` and is NOT installed in the active `backend/`.
- **Fix**: Added a `_table_exists(cr, name)` helper using `to_regclass()`. Both `list_workflows` and `list_requests` now return `{items: [], total: 0}` gracefully when the table is absent, matching the frontend's empty-state expectation.
**Bug #2: `DELETE /api/courses/<id>` returned opaque 500 for courses with enrollments**
- **Cause**: `op.course` has a `RESTRICT` FK from `op_student_course`. Postgres raised a raw violation with no user-friendly handling.
- **Fix**: `delete_course` in `encoach_lms_api/controllers/lms_core.py` now (a) detects enrollments first and returns a 409 with a clear message, (b) accepts `?force=true` to cascade-delete enrollments and detach linked batches before removing the course.
### 16.3 Admin Workflows — All Pages Pass ✅
| Page | Route | Result |
|------|-------|--------|
| Dashboard | `/admin/dashboard` | Stats cards, AI tips, platform insights render |
| Users | `/admin/users` | 7 users shown with roles, filter tabs work |
| Entities | `/admin/entities` | Demo Academy entity, 6 users / 5 roles |
| Classrooms | `/admin/classrooms` | IELTS 2026 Spring (3 students), student dialog works |
| User Roles | `/admin/user-roles` | 7 users, 5 available roles, assignments rendered |
| Roles & Permissions | `/admin/roles-permissions` | 5 roles × 115 permissions × 22 categories |
| Authority Matrix | `/admin/authority-matrix` | 257 granted cells, 45 % coverage, CSV export present |
| Courses | `/admin/courses` | 3 courses, enroll-students dialog with BOTH "Individual" and "By Classroom" tabs verified |
| Taxonomy | `/admin/taxonomy` | 3 subjects (English, Math, IT) with full tree |
| Resources | `/admin/resources` | 15 items (6 library + 9 course materials), filters & cascade pickers work |
| Exams List | `/admin/examsList` | 16 custom exams, search filters correctly |
| Generation | `/admin/generation` | Loads; Official/Practice tabs work |
| Rubrics | `/admin/rubrics` | 3 rubrics + groups tab |
| Assignments | `/admin/assignments` | 2 assignments, filter tabs work |
| Approval Workflows | `/admin/approval-workflows` | Empty-state OK, Create dialog opens |
### 16.4 Teacher Workflows — All Pages Pass ✅
Logged in as `khalid@encoach.test` (Dr. Khalid). Tested 11 pages; all work:
- `/teacher/dashboard` — 3 Active Courses, 55 Total Students, 0 Pending Grading, 82 % Avg Pass Rate
- `/teacher/courses` — 3 course cards with Edit / Chapters / AI Workbench buttons
- `/teacher/students` — 3 students with batch info, attendance, AI risk indicators
- `/teacher/library` — 15 resources, upload dialog's taxonomy cascade confirmed working
- `/teacher/assignments` — empty state handled gracefully
- `/teacher/courses/3/chapters` — empty state + "Add Chapter" dialog opens with Name/Description/Start Date/Unlock Mode
- `/teacher/attendance` — batch selector + trend chart + AI warning banners
- `/teacher/timetable` — weekly grid 08:0017:00
- `/teacher/discussions` / `/teacher/announcements` / `/teacher/profile` — all load cleanly
### 16.5 Student Workflows — All Pages Pass ✅
Logged in as `sarah@encoach.test` (password `student123`). Tested 16 pages + exam session; all work:
- `/student/dashboard` — 67 % overall progress, 3 courses, 4 upcoming exams popup
- `/student/courses` — 3 cards with progress (100 %, 100 %, 0 %)
- `/student/courses/1` — course detail with "Chapters (1) / Grades (0) / About" tabs + "Take Post-Test"
- `/student/courses/1/chapters/1` — 3/3 materials completed (IELTS Listening video, Reading article, Grammar docs)
- `/student/assignments` / `/student/grades` / `/student/attendance` / `/student/timetable` — all load with graceful empty or populated states
- `/student/profile` — editable name, email, password change
- `/student/discussions` / `/student/announcements` / `/student/messages` / `/student/journey` — empty states clean
- `/student/subjects` — 3 subjects with "Start Diagnostic" CTAs
- `/student/subject-registration` — registration table renders
**Exam flow** — `/student/exam/2/session` loaded "IELTS Academic Mock - Spring 2026 Full Test" with 170-minute timer, Listening section, 7 questions, MCQ with 4 radio options, Previous / Flag / Next navigation. Clean and functional.
### 16.6 Demo Data State (Post-Cleanup)
| Entity | Count | Notes |
|--------|-------|-------|
| Users | 7 | 2 admins, 2 teachers, 3 students |
| Courses | 3 | IELTS Preparation B2, Python Programming Fundamentals, IELTS Listening & Speaking Intensive (junk "tttttt" removed, "test" renamed) |
| Batches | 1 | IELTS 2026 Spring (3 students) |
| Exams (custom) | 16 | published + draft mix |
| Resources | 6 | library resources; +9 course materials |
| Taxonomy subjects | 3 | English (6d/37t), Mathematics (6d/15t), IT (6d/17t) — junk "math" subject removed |
### 16.7 Route Naming Gotchas
Two frontend routes differ from the pattern a developer might guess:
| Expected | Actual |
|----------|--------|
| `/admin/exams` | `/admin/examsList` |
| `/admin/roles` | `/admin/user-roles` |
The `Admin Pages` table in §14 has been updated with the full correct route list.
### 16.8 Minor Observations (Non-Blocking)
- React Router v7 future flag warnings in console on every page (`v7_startTransition`, `v7_relativeSplatPath`) — cosmetic only, ignore until v7 migration.
- forwardRef warning in Badge component — dev-only, not a runtime error.
- `/api/scores` by itself returns 404, but its sub-endpoints (`/api/scores/release` etc.) are valid.
- Session occasionally expired during long admin test runs, requiring re-login. Token TTL may warrant increasing for QA flows.
---
## 17. Institutional Admin Pages — Write-Flow QA — 2026-04-18
Comprehensive API-level CREATE / EDIT / DELETE / WORKFLOW sweep across all 14 institutional admin pages (see §14 "Admin — Institutional Pages"). Driven by `test_write_flows.py` against real Odoo data seeded by `seed_institutional.py`.
**Final result: `SUMMARY PASS=44 FAIL=0 TOTAL=44`** ✅
### 17.1 Scenarios covered
| Page | Actions verified |
|------|------------------|
| Academic Years | CREATE, generate-terms (3), EDIT, DELETE (cascades terms) |
| Academic Terms | EDIT, DELETE |
| Departments | CREATE, EDIT, DELETE |
| Admission Registers | CREATE, confirm, DELETE |
| Admissions | CREATE (date-constrained to register window), DELETE |
| Inst Exam Sessions | CREATE, schedule, EDIT, DELETE |
| Marksheets | CREATE template, generate-marksheets, validate marksheet, DELETE template (cascades registers) |
| Facilities | CREATE, EDIT, DELETE |
| Assets | CREATE, DELETE (backed by new `encoach.asset` model) |
| Activities | CREATE type, CREATE activity, DELETE |
| Library | CREATE media + authors, VERIFY author link, DELETE |
| Lessons | CREATE, EDIT (preserves `course_id` + `batch_id`), DELETE |
| Gradebook | CREATE grading-assignment, DELETE, drill lines |
| Student Progress | DRILL detail |
| Student Leaves | CREATE, approve, DELETE |
| Fees | LIST seeded, create-invoice ✅ |
### 17.2 Backend fixes applied
**1. `encoach.asset` — new lightweight model**
`op.asset` does not exist in the installed OpenEduCat set. Added `encoach_lms_api/models/asset.py` with `name`, `code`, `facility_id`, `description`, `active`; registered in `__init__.py`; CRUD access granted via `security/ir.model.access.csv`. `facilities.py` now uses `_asset_model()` → `env['encoach.asset']`.
**2. Missing DELETE routes added**
- `DELETE /api/admission-registers/<rid>`
- `DELETE /api/admissions/<aid>`
- `DELETE /api/result-templates/<tid>` (cascades `op.marksheet.register` rows created by `generate_result`)
- `DELETE /api/student-leaves/<lid>`
- `DELETE /api/academic-years/<yid>` now pre-unlinks `academic_term_ids` to avoid the `op_academic_term.academic_year_id` FK RESTRICT violation.
**3. Missing detail (GET-one) endpoints added**
- `GET /api/lessons/<lid>` — exposes full lesson incl. `course_id`, `batch_id` so EDIT can verify those are preserved.
- `GET /api/library/media/<mid>` — returns `author_ids` + `author_names` (the list endpoint only returns a comma-separated `author` string).
**4. Admissions CREATE — defaults + validation**
`create_admission` now defaults `application_date = fields.Date.today()`, builds `name` from `first_name + last_name`, and accepts `nationality_id` or `nationality`. The Odoo constraint `@api.constrains(...)` requires `application_date ∈ [register.start_date, register.end_date]` — the test payload uses `2028-06-15` to sit inside the seeded register window. Added companion `PATCH/PUT /api/admissions/<aid>` for edits.
**5. Grading assignments — required fields resolved**
`create_grading_assignment` no longer sends `user_id` (invalid on `grading.assignment` in this OpenEduCat version). Auto-resolves the required fields:
- `assignment_type`: find-or-create `grading.assignment.type` with code `DEFAULT`
- `faculty_id`: first available `op.faculty`
- `issued_date`: defaults to `fields.Datetime.now()`
**6. Marksheets generate — Odoo 19 compatibility**
Route aliased as `/api/result-templates/<tid>/generate` **and** `/generate-marksheets`. Controller tries `rec.generate_result()` (OpenEduCat 19) then falls back to `rec.action_generate_marksheet()` (older versions).
**7. Fees create-invoice — Odoo 19 JSONB properties**
OpenEduCat's `op.student.fees.details.get_invoice()` raised `UserError: There is no income account defined for this product`. Root cause: **Odoo 19 removed `ir_property` and stores company-dependent fields (like `property_account_income_id`) as JSONB columns on the model itself** — e.g. `product_template.property_account_income_id` is now `jsonb` keyed by `company_id`.
Fix: `create_invoice` auto-wires a default income account before invoking `get_invoice()`:
```241:260:backend/custom_addons/encoach_lms_api/controllers/fees.py
if not (fp or {}).get('income'):
AA = request.env['account.account'].sudo()
# Odoo 19: account.account has a M2M `company_ids`.
try:
income_acc = AA.search([
('account_type', '=', 'income'),
('company_ids', 'in', request.env.company.id),
], limit=1)
except Exception:
income_acc = AA.browse()
if not income_acc:
income_acc = AA.search([('account_type', '=', 'income')], limit=1)
if income_acc:
try:
prod_tmpl.with_company(request.env.company).write({
'property_account_income_id': income_acc.id,
})
except Exception:
_logger.exception('auto-wire income account')
```
The `seed_institutional.py` fee-product block now performs the same wiring at seed time, so fresh databases don't hit this error.
### 17.3 Seeding
`seed_institutional.py` populates the full institutional fixture set:
- 1 academic year + auto-generated terms
- 1 department
- 1 admission register + 1 admission
- 1 exam session + 1 result template → marksheets
- Facilities, activities, library media, lessons, gradebooks, student leaves
- **Fees**: 3 `op.fees.terms` (Full, 2-inst 50/50, 3-inst 34/33/33 summing to 100%), 1 `product.product` (`IELTS Preparation B2 Fees`) with income account wired, 3 `op.student.fees.details` records
Run:
```bash
cd /Users/yamenahmad/projects2026/odoo/odoo19
./run.sh --shell <<< "exec(open('seed_institutional.py').read())"
```
### 17.4 Running the write-flow test
```bash
cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_write_flows.py
```
The script is **idempotent** — it uses timestamped names for academic-year/register payloads so repeated runs don't collide on Odoo's unique constraints. Expected output: `SUMMARY PASS=44 FAIL=0 TOTAL=44`.
### 17.5 Operational notes for Odoo 19 institutional data
- `product_template.property_account_income_id` is JSONB keyed by `company_id` — write to it with `.with_company(company).write({...})`, **not** via the removed `ir_property` table.
- `op.admission` has a constraint that `application_date` must fall inside the parent `op.admission.register`'s `[start_date, end_date]` — front-end forms should default to `register.start_date` to avoid validation errors.
- `op.result.template.generate_result()` creates `op.marksheet.register` rows; deleting a template without first unlinking those rows will fail. The DELETE endpoint handles this internally.
- `op.academic.year` → `op.academic.term` FK is `RESTRICT`; always unlink terms before the year.
- `op.fees.terms.line_ids` must sum to 100% (percentages). Seeded terms conform.
## §18 Support Admin Pages — Backend + Write-Flow QA — 2026-04-18
### 18.1 Scope
The **Support** group in the admin sidebar is 3 pages:
| Admin URL | Component | Before | After (this session) |
|-------------------------------|---------------------------|------------------------------------------------|----------------------|
| `/admin/tickets` | `TicketsPage` | Called `/api/tickets` which **did not exist** (404 HTML). | Full backend — `encoach.ticket` model + `/api/tickets` CRUD + `assignedToUser`. |
| `/admin/payment-record` | `PaymentRecordPage` | Hard-coded mock arrays. | Live data from `op.student.fees.details` + `account.move` + stubbed `/api/paymob-orders`. |
| `/admin/settings-platform` | `SettingsPage` | Hard-coded mock arrays. | Codes tab → `encoach.code`, Packages tab → `ir.config_parameter`, Grading tab → `ir.config_parameter`. |
### 18.2 New backend artefacts
**Model** — `backend/custom_addons/encoach_lms_api/models/ticket.py`:
```python
class EncoachTicket(models.Model):
_name = 'encoach.ticket'
_inherit = ['mail.thread']
subject, description, type, status, source, page_context,
reporter_id, assignee_id, entity_id, resolved_at
```
Registered in `models/__init__.py` and `security/ir.model.access.csv` (full CRUD for `base.group_user`).
**Controllers** — added three new files, all registered in `controllers/__init__.py`:
| File | Routes |
|-------------------------------------|--------|
| `controllers/tickets.py` | `GET/POST /api/tickets`, `GET/PATCH/DELETE /api/tickets/<id>`, `GET /api/tickets/assignedToUser` |
| `controllers/payments.py` | `GET /api/payment-records` (derived from `op.student.fees.details`), `GET /api/payment-records/invoices` (from `account.move` out_invoice), `GET /api/paymob-orders` (stub, returns `{data:[], message:"Paymob integration not configured."}`) |
| `controllers/platform_settings.py` | `GET/POST/DELETE /api/codes`, `POST /api/codes/generate`, `GET/POST/PATCH/DELETE /api/packages`, `GET/PATCH /api/grading-config` |
Packages and grading config are persisted in `ir.config_parameter` under the keys `encoach.platform.packages` and `encoach.platform.grading` — no new model table needed.
### 18.3 Frontend changes
| File | Change |
|--------------------------------------------------|--------|
| `frontend/src/services/payments.service.ts` | **NEW** — typed client for `/api/payment-records` + `/api/paymob-orders`. |
| `frontend/src/services/platformSettings.service.ts` | **NEW** — typed client for codes, packages, grading-config. |
| `frontend/src/pages/PaymentRecordPage.tsx` | Rewritten: 4 KPI cards + live Payments table + Paymob empty-state + CSV export. Removed all mock arrays. |
| `frontend/src/pages/SettingsPage.tsx` | Rewritten: Codes tab with real Generate Single / Generate Batch and delete; Packages tab with Add Package dialog + delete; Grading tab with Save + client/server validation. |
| `frontend/src/pages/TicketsPage.tsx` | Small fix: accept both `data.items` and `data.data` from the pagination envelope. |
### 18.4 Seeding
`seed_institutional.py` now also seeds:
- **4 tickets** covering all 4 statuses (`open`, `in_progress`, `resolved`) and all main types (`bug`, `feature`, `support`, `feedback`), with realistic subjects / descriptions / `page_context`.
- **4 registration codes** (2 individual student, 2 corporate batch) — one already marked as used to exercise the badge variant.
### 18.5 Write-flow test — 29/29 PASS
`test_support_flows.py` exercises:
| Page | Actions verified |
|----------------------|------------------|
| Tickets | LIST seeded, filter by status + type, text search, CREATE, GET detail, EDIT → in_progress, EDIT → resolved (auto-stamps `resolved_at`), ASSIGN, `assignedToUser` listing, DELETE |
| Codes (Settings) | LIST seeded, generate individual ×3, filter `used=false`, DELETE, generate corporate ×2 |
| Packages (Settings) | LIST defaults, CREATE, EDIT price+discount, DELETE, persistence roundtrip |
| Grading (Settings) | GET, set new scale, server-side validation (rejects `min >= max`), restore IELTS default |
| Payment Record | LIST + totals, filter `paid=false`, invoices list (`account.move`), Paymob stub returns empty |
Run with:
```bash
cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_support_flows.py
```
Latest output: `SUMMARY PASS=29 FAIL=0 TOTAL=29`.
### 18.6 Browser verification
All 3 pages were opened in a real browser (localhost:8080) after login as `admin` / `admin` and visually verified:
- **Tickets** — 4 seeded rows visible, reporter = "Administrator", Create Ticket dialog submits and a new row appears immediately.
- **Payment Record** — 4 KPI cards (`Total records`, `Paid`, `Unpaid`, `Total amount`), 3 payment rows for the seeded students, Paymob tab shows the empty-state message.
- **Settings** — Codes tab lists all seeded + generated codes with `Generate Single` working and a success toast; Packages tab shows the 3 default cards; Grading tab shows 0..9 step 0.5 and `Save` returns a success toast.
No critical console errors — only React Router v7 future-flag warnings.
### 18.7 Operational notes
- The `encoach.ticket` model inherits `mail.thread` so OCA-style change tracking works out of the box. Adding followers / comments will work without further wiring.
- The `/api/payment-records` endpoint is **derived** (not stored) — it reflects whatever is in `op.student.fees.details`. Creating an invoice from the Fees page will flip the `paid` badge automatically on the Payment Record page once the invoice is paid.
- `/api/paymob-orders` is a deliberate empty stub. When real Paymob integration is wired, replace the stub body in `controllers/payments.py::list_paymob_orders` — no UI change required; the Payment Record page will light up automatically.
- Packages and grading config live in `ir.config_parameter`. To reset to defaults, delete the two parameters `encoach.platform.packages` and `encoach.platform.grading`.
## §19 Training Admin Pages — Backend + Write-Flow QA — 2026-04-18
### 19.1 Scope
The **Training** group in the admin sidebar is 2 pages:
| Admin URL | Component | Before | After (this session) |
|---------------------------------|--------------------|------------------------------------------|----------------------|
| `/admin/training/vocabulary` | `VocabularyPage` | Hard-coded `vocabItems` array (no API). | Full backend: CRUD + search + level filter + per-user progress tracking. |
| `/admin/training/grammar` | `GrammarPage` | Hard-coded `grammarItems` array (no API).| Full backend: CRUD + search + level filter + per-user progress tracking. |
### 19.2 New backend artefacts
**Models** — `backend/custom_addons/encoach_lms_api/models/training.py`:
| Model | Key fields |
|--------------------------------|------------|
| `encoach.vocab.item` | `word` (unique), `meaning`, `example_sentence`, `level` (CEFR A1C2), `part_of_speech`, `category`, `active`, computed `learners_count` + `completion_count` |
| `encoach.vocab.progress` | `user_id`, `vocab_id`, `completed`, `mastery` (learning/familiar/mastered), `last_reviewed`, `review_count` — unique per (user, vocab) |
| `encoach.grammar.rule` | `name` (unique), `description`, `example`, `level`, `category`, `active`, computed stats |
| `encoach.grammar.progress` | `user_id`, `rule_id`, `completed`, `last_reviewed`, `review_count` — unique per (user, rule) |
All four models are registered in `models/__init__.py` and `security/ir.model.access.csv` (full CRUD for `base.group_user`). Uniqueness is enforced via `@api.constrains` instead of `_sql_constraints` — Odoo 19 deprecated the latter (see operational notes below).
**Controller** — `backend/custom_addons/encoach_lms_api/controllers/training.py`:
| Method / Path | Verb | Purpose |
|------------------------------------------------------------|----------|---------|
| `/api/training/vocabulary` | `GET` | List with filters: `level`, `category`, `active`, `search`, pagination. Returns `items`, `total`, and a `summary` block (`total`, `completed`, `remaining`, `completion_rate`) relative to the authenticated user. |
| `/api/training/vocabulary` | `POST` | Create (`word` + `meaning` required, unique `word`). |
| `/api/training/vocabulary/<id>` | `GET` | Fetch a single item with the caller's progress merged in. |
| `/api/training/vocabulary/<id>` | `PATCH` | Update any mutable field. |
| `/api/training/vocabulary/<id>` | `DELETE` | Delete (cascades progress). |
| `/api/training/vocabulary/<id>/progress` | `POST` | Mark completed / set `mastery` for the authenticated user (creates the progress row if absent, increments `review_count`). |
| `/api/training/grammar` (+ same suffixes) | — | Analogous five endpoints for grammar rules plus `/progress`. |
`ValidationError` and `UserError` raised from the model layer now surface as **HTTP 400** (not 500), so the frontend can show user-friendly toasts.
### 19.3 Frontend changes
| File | Change |
|--------------------------------------------------|--------|
| `frontend/src/services/training.service.ts` | **NEW** — typed client for both vocabulary and grammar (list, CRUD, progress). |
| `frontend/src/pages/VocabularyPage.tsx` | Rewritten. Now: progress summary card with live % complete, search + level filter, checkable list rows, Add Word dialog with CEFR + part-of-speech, delete button per row. AI panel text is dynamic based on `summary.remaining`. |
| `frontend/src/pages/GrammarPage.tsx` | Rewritten. Same pattern as vocabulary but tailored to grammar (rule name, description, example, category). |
### 19.4 Seeding
`seed_institutional.py` now seeds:
- **10 vocabulary items** spanning B1C1, with realistic meanings and example sentences (Ubiquitous, Pragmatic, Eloquent, Meticulous, Ambiguous, Coherent, Versatile, Concise, Collaborate, Rationale).
- **8 grammar rules** spanning A2C1 (Conditionals Type 2/3, Passive Voice, Relative Clauses, Subject-Verb Agreement, Modal Speculation, Reported Speech, Articles, Present Perfect vs Past Simple).
- **6 progress rows** for the admin user (3 vocab mastered, 3 grammar completed) so the UI always shows non-zero state on a freshly seeded DB.
Run with:
```bash
.conda-envs/odoo19/bin/python odoo/odoo-bin shell -c odoo.conf --no-http <<'EOF'
exec(open('seed_institutional.py').read())
EOF
```
The seeding block is **idempotent** — re-running it never creates duplicates (each row checks by unique key first).
### 19.5 Write-flow test — 26/26 PASS
`test_training_flows.py` exercises:
| Page | Actions verified |
|-------------------|------------------|
| Vocabulary | LIST seeded, filter by `level=B2`, search `eloqu`, CREATE, **duplicate-word rejection**, **missing-meaning rejection**, GET detail, EDIT level+meaning, PROGRESS mark completed, **summary recomputation** after completion, PROGRESS un-mark, DELETE, GET-after-DELETE → 404 |
| Grammar | LIST seeded, filter by `level=B1`, search `passive`, CREATE, missing-description rejection, GET detail, EDIT level+category, PROGRESS mark / un-mark, DELETE, GET-after-DELETE → 404 |
| Pagination envelope | `size=3 page=1` (vocab), `size=3 page=2` (grammar) — confirms `page` + `size` are echoed in the response |
Run with:
```bash
cd /Users/yamenahmad/projects2026/odoo/odoo19
python3 test_training_flows.py
```
Latest output: `SUMMARY PASS=26 FAIL=0 TOTAL=26`.
### 19.6 Browser verification
Both admin pages were opened in a real browser (localhost:8080) after login as `admin` / `admin`:
- **Vocabulary** — progress card shows live `X/N completed (Y%)` with a progress bar; clicking the circle next to a row marks it complete and the summary instantly recomputes; search `"elo"` narrows the list to just *Eloquent*; the *Add Word* dialog creates a new row + shows a toast.
- **Grammar** — all 8 seeded rules visible (A2C1), progress tracking works identically; *Add Rule* dialog creates a new row.
No functional errors in the console — only React Router v7 future-flag warnings.
### 19.7 Operational notes for Odoo 19 training data
- **`_sql_constraints` is deprecated in Odoo 19** (the registry emits `Model attribute '_sql_constraints' is no longer supported`). Uniqueness and range checks in these models are therefore implemented with `@api.constrains`, which continues to work on both Odoo 18 and 19.
- Because both progress models have `ondelete='cascade'` back to the library item, deleting a vocabulary/grammar row automatically cleans up every learner's progress row — no orphan rows.
- Progress rows are keyed to `res.users.id`, not `op.student`. Any authenticated API user (admin, teacher, student) sees only their own completions in the `summary` block, while `learners_count` / `completion_count` on the library item itself aggregate across all users.
- The `/api/training/<kind>/<id>/progress` endpoint is **upsert**-like: the first call creates the progress row, subsequent calls update it and bump `review_count`. This lets the frontend treat it as "toggle" without worrying about a missing row.