feat: initial backend codebase — EnCoach v3

Complete Odoo 19 backend with 25 custom addons:
- encoach_core: user/entity/role management
- encoach_api: REST API + JWT auth
- encoach_ai: OpenAI integration, AI settings, generation
- encoach_ai_course: AI-powered English & IELTS course generation
- encoach_exam_template/session: exam creation, structures, sessions
- encoach_scoring: AI auto-grading + manual approval
- encoach_vector: pgvector RAG integration
- encoach_adaptive: adaptive learning engine
- encoach_placement: placement testing
- encoach_taxonomy/resources: content taxonomy & resource management
- Plus 14 more modules for courses, branding, portal, etc.

Includes docs: user guide, generation report, developer workflow.

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-11 15:44:20 +04:00
commit 982d4bca30
371 changed files with 35211 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
from . import gap_profile
from . import course_extension
from . import course_section
from . import course_module

View File

@@ -0,0 +1,21 @@
from odoo import models, fields
class OpCourseExtension(models.Model):
_inherit = 'op.course'
generation_source = fields.Selection([
('manual', 'Manual'),
('auto_gap', 'Auto from Gap'),
('ai_english', 'AI English'),
('ai_ielts', 'AI IELTS'),
])
gap_profile_id = fields.Many2one('encoach.gap.profile', ondelete='set null')
progression_model = fields.Selection([
('linear', 'Linear'),
('parallel', 'Parallel'),
('adaptive', 'Adaptive'),
], default='linear')
target_band = fields.Float()
study_hours_week = fields.Integer()
entity_id = fields.Many2one('encoach.entity', ondelete='set null')

View File

@@ -0,0 +1,37 @@
from odoo import models, fields
class EncoachCourseModule(models.Model):
_name = 'encoach.course.module'
_description = 'Course Module'
name = fields.Char(size=200, required=True)
course_id = fields.Many2one('op.course', required=True, ondelete='cascade')
cefr_target = fields.Selection([
('pre_a1', 'Pre-A1'),
('a1', 'A1'),
('a2', 'A2'),
('b1', 'B1'),
('b2', 'B2'),
('c1', 'C1'),
('c2', 'C2'),
])
auto_generated = fields.Boolean(default=False)
generation_brief = fields.Text()
completion_criteria = fields.Selection([
('all_resources', 'All Resources'),
('score_threshold', 'Score Threshold'),
('teacher_approval', 'Teacher Approval'),
], default='all_resources')
score_threshold = fields.Float()
prerequisite_module_id = fields.Many2one('encoach.course.module', ondelete='set null')
status = fields.Selection([
('locked', 'Locked'),
('available', 'Available'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('skipped', 'Skipped'),
], default='locked', required=True)
sequence = fields.Integer(default=10)
section_id = fields.Many2one('encoach.course.section', ondelete='set null')
resource_ids = fields.Many2many('encoach.resource', string='Resources')

View File

@@ -0,0 +1,21 @@
from odoo import models, fields
class EncoachCourseSection(models.Model):
_name = 'encoach.course.section'
_description = 'Course Section'
_order = 'sequence'
name = fields.Char(size=200, required=True)
course_id = fields.Many2one('op.course', required=True, ondelete='cascade')
skill = fields.Selection([
('listening', 'Listening'),
('reading', 'Reading'),
('writing', 'Writing'),
('speaking', 'Speaking'),
('grammar', 'Grammar'),
('vocabulary', 'Vocabulary'),
('overall', 'Overall'),
])
sequence = fields.Integer(default=10)
module_ids = fields.One2many('encoach.course.module', 'section_id', string='Modules')

View File

@@ -0,0 +1,18 @@
from odoo import models, fields
class EncoachGapProfile(models.Model):
_name = 'encoach.gap.profile'
_description = 'Student Gap Profile'
student_id = fields.Many2one('res.users', required=True, ondelete='cascade', index=True)
source_type = fields.Selection([
('placement', 'Placement'),
('exam', 'Exam'),
], required=True)
source_id = fields.Integer()
skill_gaps = fields.Text()
question_type_weaknesses = fields.Text()
topic_weaknesses = fields.Text()
entity_id = fields.Many2one('encoach.entity', ondelete='set null')
created_at = fields.Datetime(default=fields.Datetime.now)