EnCoach Odoo 19 custom modules

Full backend implementation with custom Odoo modules:
- encoach_api: Core API, user management, JWT auth
- encoach_exam: Exam generation (reading, writing, listening, speaking)
- encoach_evaluate: AI-powered evaluation (writing, speaking)
- encoach_training: Training tips and walkthrough
- encoach_storage: File storage management
- encoach_payment: Stripe, PayPal, Paymob integration
- encoach_mail: Email notifications

Made-with: Cursor
This commit is contained in:
Talal Sharabi
2026-03-14 16:46:46 +04:00
commit f5b627256f
168 changed files with 13428 additions and 0 deletions

1
encoach_exam/__init__.py Normal file
View File

@@ -0,0 +1 @@
from . import models

View File

@@ -0,0 +1,15 @@
{
"name": "EnCoach Exam",
"version": "19.0.1.0.0",
"category": "Education",
"summary": "IELTS exam models for Reading, Listening, Writing, Speaking, Level",
"depends": ["encoach_core"],
"data": [
"security/ir.model.access.csv",
"security/rules.xml",
"views/exam_views.xml",
"views/approval_views.xml",
],
"installable": True,
"license": "LGPL-3",
}

View File

@@ -0,0 +1,2 @@
from . import exam
from . import approval_workflow

View File

@@ -0,0 +1,58 @@
from odoo import models, fields
WORKFLOW_STATUS = [
("pending", "Pending"),
("approved", "Approved"),
("rejected", "Rejected"),
]
class EncoachApprovalWorkflow(models.Model):
_name = "encoach.approval.workflow"
_description = "EnCoach Exam Approval Workflow"
exam_id = fields.Many2one("encoach.exam", ondelete="cascade", index=True)
creator_id = fields.Many2one("res.users", default=lambda self: self.env.uid)
entity_id = fields.Many2one("encoach.entity")
name = fields.Char()
modules = fields.Json(help='List of modules, e.g. ["reading","writing"]')
steps = fields.Json(
help="Array of workflow step objects with stepType, stepNumber, "
"completed, rejected, completedBy, assignees, etc.",
)
status = fields.Selection(WORKFLOW_STATUS, default="pending", required=True)
start_date = fields.Datetime(default=fields.Datetime.now)
legacy_id = fields.Char(index=True)
def action_approve(self):
for rec in self:
rec.status = "approved"
if rec.exam_id:
rec.exam_id.approved = True
def action_reject(self):
for rec in self:
rec.status = "rejected"
if rec.exam_id:
rec.exam_id.approved = False
def action_reset(self):
for rec in self:
rec.status = "pending"
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"name": self.name or "",
"entityId": self.entity_id.id if self.entity_id else None,
"requester": self.creator_id.id if self.creator_id else None,
"startDate": (
int(self.start_date.timestamp() * 1000)
if self.start_date else None
),
"modules": self.modules or [],
"examId": self.exam_id.id if self.exam_id else None,
"status": self.status,
"steps": self.steps or [],
}

View File

@@ -0,0 +1,77 @@
from odoo import models, fields
MODULE_SELECTION = [
("reading", "Reading"),
("listening", "Listening"),
("writing", "Writing"),
("speaking", "Speaking"),
("level", "Level"),
]
VARIANT_SELECTION = [
("full", "Full"),
("partial", "Partial"),
]
ACCESS_SELECTION = [
("public", "Public"),
("private", "Private"),
("entity", "Entity"),
]
class EncoachExam(models.Model):
_name = "encoach.exam"
_description = "EnCoach Exam"
module = fields.Selection(MODULE_SELECTION, required=True, index=True)
min_timer = fields.Integer(string="Minimum Timer (minutes)", default=0)
is_diagnostic = fields.Boolean(default=False)
variant = fields.Selection(VARIANT_SELECTION)
difficulty = fields.Json(help="List of CEFR difficulty levels, e.g. ['A1','B2']")
owner_ids = fields.Many2many(
"res.users", "encoach_exam_owner_rel", "exam_id", "user_id",
string="Owners",
)
entity_ids = fields.Many2many(
"encoach.entity", "encoach_exam_entity_rel", "exam_id", "entity_id",
string="Entities",
)
shuffle = fields.Boolean(default=False)
access = fields.Selection(ACCESS_SELECTION, default="public", required=True)
label = fields.Char()
requires_approval = fields.Boolean(default=False)
approved = fields.Boolean(default=False)
parts = fields.Json(help="Nested exam structure (parts, exercises, questions)")
legacy_id = fields.Char(index=True)
def action_approve(self):
"""Set exam as approved (used by form view Approve button)."""
self.write({"approved": True})
def to_encoach_dict(self):
self.ensure_one()
difficulty = self.difficulty or []
if isinstance(difficulty, str):
import json
difficulty = json.loads(difficulty)
return {
"id": self.id,
"module": self.module,
"minTimer": self.min_timer,
"isDiagnostic": self.is_diagnostic,
"variant": self.variant,
"difficulty": difficulty,
"owners": self.owner_ids.ids,
"entities": [str(eid) for eid in self.entity_ids.ids],
"shuffle": self.shuffle,
"access": self.access,
"label": self.label or "",
"requiresApproval": self.requires_approval,
"approved": self.approved,
"parts": self.parts or [],
"createdBy": self.create_uid.id if self.create_uid else None,
"createdAt": (
self.create_date.isoformat() if self.create_date else None
),
}

View File

@@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_encoach_exam_student,encoach.exam.student,model_encoach_exam,encoach_core.group_encoach_student,1,0,0,0
access_encoach_exam_teacher,encoach.exam.teacher,model_encoach_exam,encoach_core.group_encoach_teacher,1,1,1,0
access_encoach_exam_admin,encoach.exam.admin,model_encoach_exam,encoach_core.group_encoach_admin,1,1,1,1
access_encoach_approval_workflow_student,encoach.approval.workflow.student,model_encoach_approval_workflow,encoach_core.group_encoach_student,1,0,0,0
access_encoach_approval_workflow_teacher,encoach.approval.workflow.teacher,model_encoach_approval_workflow,encoach_core.group_encoach_teacher,1,1,1,0
access_encoach_approval_workflow_admin,encoach.approval.workflow.admin,model_encoach_approval_workflow,encoach_core.group_encoach_admin,1,1,1,1
access_encoach_exam_sysadmin,encoach.exam.sysadmin,model_encoach_exam,base.group_system,1,1,1,1
access_encoach_approval_workflow_sysadmin,encoach.approval.workflow.sysadmin,model_encoach_approval_workflow,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_encoach_exam_student encoach.exam.student model_encoach_exam encoach_core.group_encoach_student 1 0 0 0
3 access_encoach_exam_teacher encoach.exam.teacher model_encoach_exam encoach_core.group_encoach_teacher 1 1 1 0
4 access_encoach_exam_admin encoach.exam.admin model_encoach_exam encoach_core.group_encoach_admin 1 1 1 1
5 access_encoach_approval_workflow_student encoach.approval.workflow.student model_encoach_approval_workflow encoach_core.group_encoach_student 1 0 0 0
6 access_encoach_approval_workflow_teacher encoach.approval.workflow.teacher model_encoach_approval_workflow encoach_core.group_encoach_teacher 1 1 1 0
7 access_encoach_approval_workflow_admin encoach.approval.workflow.admin model_encoach_approval_workflow encoach_core.group_encoach_admin 1 1 1 1
8 access_encoach_exam_sysadmin encoach.exam.sysadmin model_encoach_exam base.group_system 1 1 1 1
9 access_encoach_approval_workflow_sysadmin encoach.approval.workflow.sysadmin model_encoach_approval_workflow base.group_system 1 1 1 1

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="rule_exam_public" model="ir.rule">
<field name="name">Students see public exams</field>
<field name="model_id" ref="model_encoach_exam"/>
<field name="groups" eval="[(4, ref('encoach_core.group_encoach_student'))]"/>
<field name="domain_force">['|', ('access', '=', 'public'), ('owner_ids', 'in', user.id)]</field>
</record>
<record id="rule_exam_teacher" model="ir.rule">
<field name="name">Teachers see all exams</field>
<field name="model_id" ref="model_encoach_exam"/>
<field name="groups" eval="[(4, ref('encoach_core.group_encoach_teacher'))]"/>
<field name="domain_force">[(1, '=', 1)]</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_encoach_approval_workflow_tree" model="ir.ui.view">
<field name="name">encoach.approval.workflow.tree</field>
<field name="model">encoach.approval.workflow</field>
<field name="arch" type="xml">
<list string="Approval Workflows">
<field name="exam_id"/>
<field name="creator_id"/>
<field name="status"/>
<field name="modules"/>
<field name="steps"/>
</list>
</field>
</record>
<record id="view_encoach_approval_workflow_form" model="ir.ui.view">
<field name="name">encoach.approval.workflow.form</field>
<field name="model">encoach.approval.workflow</field>
<field name="arch" type="xml">
<form string="Approval Workflow">
<header>
<button name="action_approve" string="Approve" type="object"
class="btn-primary" invisible="status != 'pending'"/>
<button name="action_reject" string="Reject" type="object"
invisible="status != 'pending'"/>
<button name="action_reset" string="Reset" type="object"
invisible="status == 'pending'"/>
</header>
<sheet>
<group>
<group>
<field name="exam_id"/>
<field name="creator_id"/>
<field name="status"/>
</group>
<group>
<field name="entity_id"/>
<field name="name"/>
<field name="start_date"/>
</group>
<group>
<field name="modules" widget="json"/>
<field name="steps" widget="json"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_encoach_approval_workflow" model="ir.actions.act_window">
<field name="name">Approval Workflows</field>
<field name="res_model">encoach.approval.workflow</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first approval workflow.
</p>
<p>
Approval workflows manage the exam approval process across modules.
</p>
</field>
</record>
<menuitem id="menu_encoach_approval_workflow"
name="Approval Workflows"
parent="encoach_core.menu_encoach_exams"
action="action_encoach_approval_workflow"
sequence="20"/>
</odoo>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="view_encoach_exam_tree" model="ir.ui.view">
<field name="name">encoach.exam.tree</field>
<field name="model">encoach.exam</field>
<field name="arch" type="xml">
<list string="Exams">
<field name="module" widget="badge"/>
<field name="label"/>
<field name="access"/>
<field name="variant"/>
<field name="min_timer"/>
<field name="is_diagnostic"/>
<field name="requires_approval"/>
<field name="approved"/>
<field name="shuffle"/>
<field name="create_date"/>
</list>
</field>
</record>
<record id="view_encoach_exam_form" model="ir.ui.view">
<field name="name">encoach.exam.form</field>
<field name="model">encoach.exam</field>
<field name="arch" type="xml">
<form string="Exam">
<header>
<button name="action_approve" string="Approve" type="object"
class="btn-primary"
invisible="not requires_approval or approved"/>
</header>
<sheet>
<group>
<group>
<field name="label"/>
<field name="module"/>
</group>
<group>
<field name="access"/>
<field name="variant"/>
<field name="min_timer"/>
</group>
<group>
<field name="is_diagnostic"/>
<field name="shuffle"/>
<field name="requires_approval"/>
<field name="approved"/>
</group>
<group>
<field name="owner_ids" widget="many2many_tags"/>
<field name="entity_ids" widget="many2many_tags"/>
</group>
</group>
<notebook>
<page string="Parts (JSON)" name="parts">
<field name="parts" widget="json" nolabel="1" placeholder="{}"/>
</page>
<page string="Difficulty" name="difficulty">
<field name="difficulty" widget="json" nolabel="1" placeholder="[]"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_encoach_exam" model="ir.actions.act_window">
<field name="name">Exams</field>
<field name="res_model">encoach.exam</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first exam.
</p>
<p>
Exams represent IELTS exam configurations for Reading, Listening,
Writing, Speaking, or Level modules.
</p>
</field>
</record>
<menuitem id="menu_encoach_exam_all"
name="All Exams"
parent="encoach_core.menu_encoach_exams"
action="action_encoach_exam"
sequence="10"/>
</odoo>