feat(v3): restructure project + add complete frontend

- Restructure: move backend from new_project/ to backend/
- Add full React/TypeScript frontend (37 pages, 17 services, 16 type defs, 11 query hooks)
- Add docs/ with SRS specs, user stories, and workflow documentation
- Update .gitignore for new directory layout

Workflows implemented:
  WF1 User Signup, WF2 Placement Test, WF3 Exam Configuration,
  WF4 General English Exam, WF5 Course Generation,
  WF6 Entity Student Onboarding, AI Course Generation,
  Adaptive Learning Engine UI, White-Label Branding, Score Release

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-10 17:26:42 +04:00
commit 907a5c0e92
331 changed files with 23511 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,27 @@
{
'name': 'EnCoach Core',
'version': '19.0.1.1',
'category': 'Education',
'summary': 'Core models for EnCoach: users, entities, roles, permissions',
'author': 'EnCoach',
'depends': ['base', 'mail', 'openeducat_core'],
'data': [
'security/encoach_security.xml',
'security/ir.model.access.csv',
'data/encoach_permissions.xml',
'views/encoach_menus.xml',
'views/encoach_entity_views.xml',
'views/entity_level_mapping_views.xml',
'views/dashboard_action.xml',
],
'assets': {
'web.assets_backend': [
'encoach_core/static/src/js/dashboard.js',
'encoach_core/static/src/xml/dashboard.xml',
'encoach_core/static/src/css/dashboard.css',
],
},
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View File

@@ -0,0 +1,468 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="0">
<!-- Global permissions (PermissionType from permissions.ts) -->
<record id="perm_viewCorporate" model="encoach.permission">
<field name="code">viewCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_editCorporate" model="encoach.permission">
<field name="code">editCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_deleteCorporate" model="encoach.permission">
<field name="code">deleteCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_createCodeCorporate" model="encoach.permission">
<field name="code">createCodeCorporate</field>
<field name="topic">Manage Corporate</field>
</record>
<record id="perm_viewAdmin" model="encoach.permission">
<field name="code">viewAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_editAdmin" model="encoach.permission">
<field name="code">editAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_deleteAdmin" model="encoach.permission">
<field name="code">deleteAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_createCodeAdmin" model="encoach.permission">
<field name="code">createCodeAdmin</field>
<field name="topic">Manage Admin</field>
</record>
<record id="perm_viewStudent" model="encoach.permission">
<field name="code">viewStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_editStudent" model="encoach.permission">
<field name="code">editStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_deleteStudent" model="encoach.permission">
<field name="code">deleteStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_createCodeStudent" model="encoach.permission">
<field name="code">createCodeStudent</field>
<field name="topic">Manage Student</field>
</record>
<record id="perm_viewTeacher" model="encoach.permission">
<field name="code">viewTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_editTeacher" model="encoach.permission">
<field name="code">editTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_deleteTeacher" model="encoach.permission">
<field name="code">deleteTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_createCodeTeacher" model="encoach.permission">
<field name="code">createCodeTeacher</field>
<field name="topic">Manage Teacher</field>
</record>
<record id="perm_viewCountryManager" model="encoach.permission">
<field name="code">viewCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_editCountryManager" model="encoach.permission">
<field name="code">editCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_deleteCountryManager" model="encoach.permission">
<field name="code">deleteCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_createCodeCountryManager" model="encoach.permission">
<field name="code">createCodeCountryManager</field>
<field name="topic">Manage Country Manager</field>
</record>
<record id="perm_createReadingExam" model="encoach.permission">
<field name="code">createReadingExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createListeningExam" model="encoach.permission">
<field name="code">createListeningExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createWritingExam" model="encoach.permission">
<field name="code">createWritingExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createSpeakingExam" model="encoach.permission">
<field name="code">createSpeakingExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_createLevelExam" model="encoach.permission">
<field name="code">createLevelExam</field>
<field name="topic">Manage Exams</field>
</record>
<record id="perm_viewExams" model="encoach.permission">
<field name="code">viewExams</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewExercises" model="encoach.permission">
<field name="code">viewExercises</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewRecords" model="encoach.permission">
<field name="code">viewRecords</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewStats" model="encoach.permission">
<field name="code">viewStats</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewTickets" model="encoach.permission">
<field name="code">viewTickets</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewPaymentRecords" model="encoach.permission">
<field name="code">viewPaymentRecords</field>
<field name="topic">View Pages</field>
</record>
<record id="perm_viewGroup" model="encoach.permission">
<field name="code">viewGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_editGroup" model="encoach.permission">
<field name="code">editGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_deleteGroup" model="encoach.permission">
<field name="code">deleteGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_createGroup" model="encoach.permission">
<field name="code">createGroup</field>
<field name="topic">Manage Group</field>
</record>
<record id="perm_viewCodes" model="encoach.permission">
<field name="code">viewCodes</field>
<field name="topic">Manage Codes</field>
</record>
<record id="perm_deleteCodes" model="encoach.permission">
<field name="code">deleteCodes</field>
<field name="topic">Manage Codes</field>
</record>
<record id="perm_createCodes" model="encoach.permission">
<field name="code">createCodes</field>
<field name="topic">Manage Codes</field>
</record>
<record id="perm_all" model="encoach.permission">
<field name="code">all</field>
<field name="topic">Others</field>
</record>
<!-- Role-level permissions (RolePermission from entityPermissions.ts) -->
<record id="rperm_view_students" model="encoach.permission">
<field name="code">view_students</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_teachers" model="encoach.permission">
<field name="code">view_teachers</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_corporates" model="encoach.permission">
<field name="code">view_corporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_mastercorporates" model="encoach.permission">
<field name="code">view_mastercorporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_students" model="encoach.permission">
<field name="code">edit_students</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_teachers" model="encoach.permission">
<field name="code">edit_teachers</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_corporates" model="encoach.permission">
<field name="code">edit_corporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_edit_mastercorporates" model="encoach.permission">
<field name="code">edit_mastercorporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_students" model="encoach.permission">
<field name="code">delete_students</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_teachers" model="encoach.permission">
<field name="code">delete_teachers</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_corporates" model="encoach.permission">
<field name="code">delete_corporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_delete_mastercorporates" model="encoach.permission">
<field name="code">delete_mastercorporates</field>
<field name="topic">Users</field>
</record>
<record id="rperm_generate_reading" model="encoach.permission">
<field name="code">generate_reading</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_reading" model="encoach.permission">
<field name="code">view_reading</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_reading" model="encoach.permission">
<field name="code">delete_reading</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_listening" model="encoach.permission">
<field name="code">generate_listening</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_listening" model="encoach.permission">
<field name="code">view_listening</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_listening" model="encoach.permission">
<field name="code">delete_listening</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_writing" model="encoach.permission">
<field name="code">generate_writing</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_writing" model="encoach.permission">
<field name="code">view_writing</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_writing" model="encoach.permission">
<field name="code">delete_writing</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_speaking" model="encoach.permission">
<field name="code">generate_speaking</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_speaking" model="encoach.permission">
<field name="code">view_speaking</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_speaking" model="encoach.permission">
<field name="code">delete_speaking</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_generate_level" model="encoach.permission">
<field name="code">generate_level</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_level" model="encoach.permission">
<field name="code">view_level</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_delete_level" model="encoach.permission">
<field name="code">delete_level</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_classrooms" model="encoach.permission">
<field name="code">view_classrooms</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_create_classroom" model="encoach.permission">
<field name="code">create_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_rename_classrooms" model="encoach.permission">
<field name="code">rename_classrooms</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_add_to_classroom" model="encoach.permission">
<field name="code">add_to_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_remove_from_classroom" model="encoach.permission">
<field name="code">remove_from_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_delete_classroom" model="encoach.permission">
<field name="code">delete_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_view_entities" model="encoach.permission">
<field name="code">view_entities</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_rename_entity" model="encoach.permission">
<field name="code">rename_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_add_to_entity" model="encoach.permission">
<field name="code">add_to_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_remove_from_entity" model="encoach.permission">
<field name="code">remove_from_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_delete_entity" model="encoach.permission">
<field name="code">delete_entity</field>
<field name="topic">Entities</field>
</record>
<record id="rperm_view_entity_roles" model="encoach.permission">
<field name="code">view_entity_roles</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_create_entity_role" model="encoach.permission">
<field name="code">create_entity_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_rename_entity_role" model="encoach.permission">
<field name="code">rename_entity_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_edit_role_permissions" model="encoach.permission">
<field name="code">edit_role_permissions</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_assign_to_role" model="encoach.permission">
<field name="code">assign_to_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_delete_entity_role" model="encoach.permission">
<field name="code">delete_entity_role</field>
<field name="topic">Roles</field>
</record>
<record id="rperm_view_assignments" model="encoach.permission">
<field name="code">view_assignments</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_create_assignment" model="encoach.permission">
<field name="code">create_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_edit_assignment" model="encoach.permission">
<field name="code">edit_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_delete_assignment" model="encoach.permission">
<field name="code">delete_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_start_assignment" model="encoach.permission">
<field name="code">start_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_archive_assignment" model="encoach.permission">
<field name="code">archive_assignment</field>
<field name="topic">Assignments</field>
</record>
<record id="rperm_view_entity_statistics" model="encoach.permission">
<field name="code">view_entity_statistics</field>
<field name="topic">Statistics</field>
</record>
<record id="rperm_create_user" model="encoach.permission">
<field name="code">create_user</field>
<field name="topic">Users</field>
</record>
<record id="rperm_create_user_batch" model="encoach.permission">
<field name="code">create_user_batch</field>
<field name="topic">Users</field>
</record>
<record id="rperm_create_code" model="encoach.permission">
<field name="code">create_code</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_create_code_batch" model="encoach.permission">
<field name="code">create_code_batch</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_view_code_list" model="encoach.permission">
<field name="code">view_code_list</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_delete_code" model="encoach.permission">
<field name="code">delete_code</field>
<field name="topic">Codes</field>
</record>
<record id="rperm_view_statistics" model="encoach.permission">
<field name="code">view_statistics</field>
<field name="topic">Statistics</field>
</record>
<record id="rperm_download_statistics_report" model="encoach.permission">
<field name="code">download_statistics_report</field>
<field name="topic">Statistics</field>
</record>
<record id="rperm_edit_grading_system" model="encoach.permission">
<field name="code">edit_grading_system</field>
<field name="topic">Grading</field>
</record>
<record id="rperm_view_student_performance" model="encoach.permission">
<field name="code">view_student_performance</field>
<field name="topic">Students</field>
</record>
<record id="rperm_upload_classroom" model="encoach.permission">
<field name="code">upload_classroom</field>
<field name="topic">Classrooms</field>
</record>
<record id="rperm_download_user_list" model="encoach.permission">
<field name="code">download_user_list</field>
<field name="topic">Users</field>
</record>
<record id="rperm_view_student_record" model="encoach.permission">
<field name="code">view_student_record</field>
<field name="topic">Students</field>
</record>
<record id="rperm_download_student_record" model="encoach.permission">
<field name="code">download_student_record</field>
<field name="topic">Students</field>
</record>
<record id="rperm_pay_entity" model="encoach.permission">
<field name="code">pay_entity</field>
<field name="topic">Payments</field>
</record>
<record id="rperm_view_payment_record" model="encoach.permission">
<field name="code">view_payment_record</field>
<field name="topic">Payments</field>
</record>
<record id="rperm_view_approval_workflows" model="encoach.permission">
<field name="code">view_approval_workflows</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_update_exam_privacy" model="encoach.permission">
<field name="code">update_exam_privacy</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_view_workflows" model="encoach.permission">
<field name="code">view_workflows</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_configure_workflows" model="encoach.permission">
<field name="code">configure_workflows</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_edit_workflow" model="encoach.permission">
<field name="code">edit_workflow</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_delete_workflow" model="encoach.permission">
<field name="code">delete_workflow</field>
<field name="topic">Workflows</field>
</record>
<record id="rperm_view_confidential_exams" model="encoach.permission">
<field name="code">view_confidential_exams</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_create_confidential_exams" model="encoach.permission">
<field name="code">create_confidential_exams</field>
<field name="topic">Exams</field>
</record>
<record id="rperm_create_public_exams" model="encoach.permission">
<field name="code">create_public_exams</field>
<field name="topic">Exams</field>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,8 @@
from . import encoach_user
from . import encoach_entity
from . import role
from . import permission
from . import code
from . import invite
from . import user_entity_rel
from . import entity_level_mapping

View File

@@ -0,0 +1,57 @@
import uuid
from odoo import models, fields, api
class EncoachCode(models.Model):
_name = "encoach.code"
_description = "EnCoach Registration Code"
code = fields.Char(required=True, index=True, default=lambda self: uuid.uuid4().hex[:8].upper())
entity_id = fields.Many2one("encoach.entity", ondelete="set null")
creator_id = fields.Many2one("res.users", required=True)
expiry_date = fields.Datetime()
code_type = fields.Selection(
[("individual", "Individual"), ("corporate", "Corporate")],
default="individual",
)
max_uses = fields.Integer(default=1)
uses = fields.Integer(default=0)
user_type = fields.Selection(
[
("student", "Student"),
("teacher", "Teacher"),
("corporate", "Corporate"),
],
default="student",
)
legacy_id = fields.Char(index=True)
@api.model
def validate_code(self, code_str):
rec = self.search([("code", "=", code_str)], limit=1)
if not rec:
return {"valid": False, "error": "Code not found"}
if rec.expiry_date and rec.expiry_date < fields.Datetime.now():
return {"valid": False, "error": "Code expired"}
if rec.max_uses and rec.uses >= rec.max_uses:
return {"valid": False, "error": "Code fully used"}
return {
"valid": True,
"code": rec.to_encoach_dict(),
}
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"code": self.code,
"entity": self.entity_id.id if self.entity_id else None,
"creator": self.creator_id.id,
"expiryDate": (
self.expiry_date.isoformat() if self.expiry_date else None
),
"type": self.code_type,
"maxUses": self.max_uses,
"uses": self.uses,
"userType": self.user_type,
}

View File

@@ -0,0 +1,62 @@
from odoo import models, fields, api
class EncoachEntity(models.Model):
_name = 'encoach.entity'
_description = 'EnCoach Entity'
_inherit = ['mail.thread']
name = fields.Char(required=True, tracking=True)
code = fields.Char(required=True, tracking=True)
type = fields.Selection([
('corporate', 'Corporate'),
('university', 'University'),
('school', 'School'),
('government', 'Government'),
('freelance', 'Freelance'),
], tracking=True)
logo = fields.Binary(attachment=True)
logo_url = fields.Char(string='Logo URL')
primary_color = fields.Char(default='#4F46E5')
secondary_color = fields.Char(default='#7C3AED')
background_color = fields.Char(default='#FFFFFF')
white_label_domain = fields.Char(string='White Label Domain')
login_title = fields.Char()
login_description = fields.Text()
favicon = fields.Binary(attachment=True)
results_release_mode = fields.Selection([
('auto', 'Auto'),
('manual_approval', 'Manual Approval'),
], default='auto')
active = fields.Boolean(default=True)
user_ids = fields.Many2many('res.users', 'encoach_entity_user_rel', 'entity_id', 'user_id', string='Users')
role_ids = fields.One2many('encoach.role', 'entity_id', string='Roles')
user_count = fields.Integer(compute='_compute_user_count', store=True)
_code_unique = models.Constraint('UNIQUE(code)', 'Entity code must be unique.')
@api.depends('user_ids')
def _compute_user_count(self):
for rec in self:
rec.user_count = len(rec.user_ids)
def to_api_dict(self):
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'code': self.code,
'type': self.type or '',
'logo': bool(self.logo),
'logo_url': self.logo_url or '',
'primary_color': self.primary_color or '#4F46E5',
'secondary_color': self.secondary_color or '#7C3AED',
'background_color': self.background_color or '#FFFFFF',
'white_label_domain': self.white_label_domain or '',
'login_title': self.login_title or '',
'login_description': self.login_description or '',
'has_favicon': bool(self.favicon),
'results_release_mode': self.results_release_mode or 'auto',
'active': self.active,
'user_count': self.user_count,
}

View File

@@ -0,0 +1,86 @@
from odoo import models, fields, api
USER_TYPE_SELECTION = [
('student', 'Student'),
('teacher', 'Teacher'),
('admin', 'Admin'),
('corporate', 'Corporate'),
('mastercorporate', 'Master Corporate'),
('agent', 'Agent'),
('developer', 'Developer'),
]
class EncoachUser(models.Model):
_inherit = 'res.users'
user_type = fields.Selection(USER_TYPE_SELECTION, string='User Type')
is_verified = fields.Boolean(default=False)
entity_ids = fields.Many2many('encoach.entity', 'encoach_entity_user_rel', 'user_id', 'entity_id', string='Entities')
phone = fields.Char()
birth_date = fields.Date()
gender = fields.Selection([('m', 'Male'), ('f', 'Female'), ('o', 'Other')])
op_student_id = fields.Many2one('op.student', string='OpenEduCat Student')
op_faculty_id = fields.Many2one('op.faculty', string='OpenEduCat Faculty')
encoach_avatar = fields.Binary(attachment=True, string='EnCoach Avatar')
last_login = fields.Datetime()
role_ids = fields.Many2many(
'encoach.role', 'encoach_role_user_rel', 'user_id', 'role_id', string='Roles',
)
permission_direct_ids = fields.Many2many(
'encoach.permission', 'encoach_user_permission_rel', 'user_id', 'permission_id',
string='Direct Permissions',
)
first_login = fields.Boolean(default=True)
account_source = fields.Selection([
('self_registered', 'Self Registered'),
('entity_bulk_upload', 'Entity Bulk Upload'),
])
account_status = fields.Selection([
('unactivated', 'Unactivated'),
('activated', 'Activated'),
('suspended', 'Suspended'),
], default='unactivated')
def get_all_permissions(self):
"""Return the union of direct permissions and permissions from all assigned roles."""
self.ensure_one()
role_permissions = self.role_ids.mapped('permission_ids')
return self.permission_direct_ids | role_permissions
def _api_user_type(self):
"""Frontend routes require a non-empty role; many users have no encoach.user_type set yet."""
self.ensure_one()
if self.user_type:
return self.user_type
if self.op_faculty_id:
return 'teacher'
if self.op_student_id:
return 'student'
if self.has_group('base.group_system'):
return 'admin'
return ''
def to_api_dict(self):
self.ensure_one()
return {
'id': self.id,
'name': self.name,
'email': self.email or '',
'login': self.login or '',
'user_type': self._api_user_type(),
'is_verified': self.is_verified,
'phone': self.phone or '',
'birth_date': self.birth_date.isoformat() if self.birth_date else None,
'gender': self.gender or '',
'avatar': bool(self.encoach_avatar),
'entities': [
{'id': e.id, 'name': e.name, 'logo': bool(e.logo)}
for e in self.entity_ids
],
'last_login': self.last_login.isoformat() if self.last_login else None,
'first_login': self.first_login,
'account_source': self.account_source or '',
'account_status': self.account_status or 'unactivated',
}

View File

@@ -0,0 +1,15 @@
from odoo import models, fields
class EncoachEntityLevelMapping(models.Model):
_name = 'encoach.entity.level.mapping'
_description = 'Entity Level Mapping'
entity_id = fields.Many2one('encoach.entity', required=True, ondelete='cascade', index=True)
min_score = fields.Float(required=True)
max_score = fields.Float(required=True)
internal_level_name = fields.Char(size=100, required=True)
cefr_equivalent = fields.Selection([
('pre_a1', 'Pre-A1'), ('a1', 'A1'), ('a2', 'A2'),
('b1', 'B1'), ('b2', 'B2'), ('c1', 'C1'), ('c2', 'C2'),
])

View File

@@ -0,0 +1,46 @@
from odoo import models, fields
class EncoachInvite(models.Model):
_name = "encoach.invite"
_description = "EnCoach Entity Invite"
from_user_id = fields.Many2one("res.users", required=True, ondelete="cascade")
to_user_id = fields.Many2one("res.users", required=True, ondelete="cascade")
entity_id = fields.Many2one("encoach.entity", required=True, ondelete="cascade")
status = fields.Selection(
[
("pending", "Pending"),
("accepted", "Accepted"),
("declined", "Declined"),
],
default="pending",
)
legacy_id = fields.Char(index=True)
def action_accept(self):
self.ensure_one()
self.status = "accepted"
existing = self.env["encoach.user.entity.rel"].search([
("user_id", "=", self.to_user_id.id),
("entity_id", "=", self.entity_id.id),
], limit=1)
if not existing:
self.env["encoach.user.entity.rel"].create({
"user_id": self.to_user_id.id,
"entity_id": self.entity_id.id,
})
def action_decline(self):
self.ensure_one()
self.status = "declined"
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"from": self.from_user_id.id,
"to": self.to_user_id.id,
"entity": self.entity_id.id,
"status": self.status,
}

View File

@@ -0,0 +1,24 @@
from odoo import models, fields
class EncoachPermission(models.Model):
_name = "encoach.permission"
_description = "EnCoach Permission"
code = fields.Char(required=True, index=True)
topic = fields.Char()
legacy_id = fields.Char(index=True)
_code_unique = models.Constraint(
"UNIQUE(code)",
"Permission code must be unique.",
)
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"type": self.code,
"topic": self.topic or "",
"users": [],
}

View File

@@ -0,0 +1,22 @@
from odoo import models, fields
class EncoachRole(models.Model):
_name = "encoach.role"
_description = "EnCoach Role"
name = fields.Char(required=True)
entity_id = fields.Many2one("encoach.entity", required=True, ondelete="cascade")
permission_ids = fields.Many2many("encoach.permission", string="Permissions")
is_default = fields.Boolean(default=False)
legacy_id = fields.Char(index=True)
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"label": self.name,
"entityID": self.entity_id.id,
"permissions": [p.code for p in self.permission_ids if p.code],
"isDefault": self.is_default,
}

View File

@@ -0,0 +1,15 @@
from odoo import models, fields
class EncoachUserEntityRel(models.Model):
_name = "encoach.user.entity.rel"
_description = "EnCoach User-Entity Membership"
user_id = fields.Many2one("res.users", required=True, ondelete="cascade")
entity_id = fields.Many2one("encoach.entity", required=True, ondelete="cascade")
role_id = fields.Many2one("encoach.role", ondelete="set null")
_user_entity_unique = models.Constraint(
"UNIQUE(user_id, entity_id)",
"A user can only have one membership per entity.",
)

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<!-- Security groups matching the 7 user types -->
<record id="group_encoach_student" model="res.groups">
<field name="name">EnCoach Student</field>
</record>
<record id="group_encoach_teacher" model="res.groups">
<field name="name">EnCoach Teacher</field>
</record>
<record id="group_encoach_corporate" model="res.groups">
<field name="name">EnCoach Corporate</field>
</record>
<record id="group_encoach_admin" model="res.groups">
<field name="name">EnCoach Admin</field>
<field name="implied_ids" eval="[(4, ref('group_encoach_teacher'))]"/>
</record>
<record id="group_encoach_developer" model="res.groups">
<field name="name">EnCoach Developer</field>
<field name="implied_ids" eval="[(4, ref('group_encoach_admin'))]"/>
</record>
<record id="group_encoach_agent" model="res.groups">
<field name="name">EnCoach Agent</field>
</record>
<record id="group_encoach_mastercorporate" model="res.groups">
<field name="name">EnCoach Master Corporate</field>
<field name="implied_ids" eval="[(4, ref('group_encoach_corporate'))]"/>
</record>
</data>
</odoo>

View File

@@ -0,0 +1,20 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_encoach_entity_student,encoach.entity.student,model_encoach_entity,group_encoach_student,1,0,0,0
access_encoach_entity_teacher,encoach.entity.teacher,model_encoach_entity,group_encoach_teacher,1,1,0,0
access_encoach_entity_admin,encoach.entity.admin,model_encoach_entity,group_encoach_admin,1,1,1,1
access_encoach_role_student,encoach.role.student,model_encoach_role,group_encoach_student,1,0,0,0
access_encoach_role_teacher,encoach.role.teacher,model_encoach_role,group_encoach_teacher,1,1,0,0
access_encoach_role_admin,encoach.role.admin,model_encoach_role,group_encoach_admin,1,1,1,1
access_encoach_permission_student,encoach.permission.student,model_encoach_permission,group_encoach_student,1,0,0,0
access_encoach_permission_teacher,encoach.permission.teacher,model_encoach_permission,group_encoach_teacher,1,0,0,0
access_encoach_permission_admin,encoach.permission.admin,model_encoach_permission,group_encoach_admin,1,1,1,1
access_encoach_invite_student,encoach.invite.student,model_encoach_invite,group_encoach_student,1,0,0,0
access_encoach_invite_teacher,encoach.invite.teacher,model_encoach_invite,group_encoach_teacher,1,1,0,0
access_encoach_invite_admin,encoach.invite.admin,model_encoach_invite,group_encoach_admin,1,1,1,1
access_encoach_code_student,encoach.code.student,model_encoach_code,group_encoach_student,1,0,0,0
access_encoach_code_teacher,encoach.code.teacher,model_encoach_code,group_encoach_teacher,1,1,0,0
access_encoach_code_admin,encoach.code.admin,model_encoach_code,group_encoach_admin,1,1,1,1
access_encoach_user_entity_rel_all,encoach.user.entity.rel.all,model_encoach_user_entity_rel,base.group_user,1,1,1,1
access_encoach_level_mapping_student,encoach.entity.level.mapping.student,model_encoach_entity_level_mapping,group_encoach_student,1,0,0,0
access_encoach_level_mapping_teacher,encoach.entity.level.mapping.teacher,model_encoach_entity_level_mapping,group_encoach_teacher,1,1,0,0
access_encoach_level_mapping_admin,encoach.entity.level.mapping.admin,model_encoach_entity_level_mapping,group_encoach_admin,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_encoach_entity_student encoach.entity.student model_encoach_entity group_encoach_student 1 0 0 0
3 access_encoach_entity_teacher encoach.entity.teacher model_encoach_entity group_encoach_teacher 1 1 0 0
4 access_encoach_entity_admin encoach.entity.admin model_encoach_entity group_encoach_admin 1 1 1 1
5 access_encoach_role_student encoach.role.student model_encoach_role group_encoach_student 1 0 0 0
6 access_encoach_role_teacher encoach.role.teacher model_encoach_role group_encoach_teacher 1 1 0 0
7 access_encoach_role_admin encoach.role.admin model_encoach_role group_encoach_admin 1 1 1 1
8 access_encoach_permission_student encoach.permission.student model_encoach_permission group_encoach_student 1 0 0 0
9 access_encoach_permission_teacher encoach.permission.teacher model_encoach_permission group_encoach_teacher 1 0 0 0
10 access_encoach_permission_admin encoach.permission.admin model_encoach_permission group_encoach_admin 1 1 1 1
11 access_encoach_invite_student encoach.invite.student model_encoach_invite group_encoach_student 1 0 0 0
12 access_encoach_invite_teacher encoach.invite.teacher model_encoach_invite group_encoach_teacher 1 1 0 0
13 access_encoach_invite_admin encoach.invite.admin model_encoach_invite group_encoach_admin 1 1 1 1
14 access_encoach_code_student encoach.code.student model_encoach_code group_encoach_student 1 0 0 0
15 access_encoach_code_teacher encoach.code.teacher model_encoach_code group_encoach_teacher 1 1 0 0
16 access_encoach_code_admin encoach.code.admin model_encoach_code group_encoach_admin 1 1 1 1
17 access_encoach_user_entity_rel_all encoach.user.entity.rel.all model_encoach_user_entity_rel base.group_user 1 1 1 1
18 access_encoach_level_mapping_student encoach.entity.level.mapping.student model_encoach_entity_level_mapping group_encoach_student 1 0 0 0
19 access_encoach_level_mapping_teacher encoach.entity.level.mapping.teacher model_encoach_entity_level_mapping group_encoach_teacher 1 1 0 0
20 access_encoach_level_mapping_admin encoach.entity.level.mapping.admin model_encoach_entity_level_mapping group_encoach_admin 1 1 1 1

View File

@@ -0,0 +1,16 @@
.o_encoach_dashboard .card {
transition: transform 0.15s, box-shadow 0.15s;
border-radius: 0.5rem;
}
.o_encoach_dashboard .card.cursor-pointer:hover {
transform: translateY(-2px);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1) !important;
}
.o_encoach_dashboard h2 {
color: #1f2937;
font-weight: 700;
}
.o_encoach_dashboard .badge {
text-transform: capitalize;
font-size: 0.75rem;
}

View File

@@ -0,0 +1,94 @@
/** @odoo-module */
import { Component, onWillStart, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
class EncoachDashboard extends Component {
static template = "encoach_core.Dashboard";
static components = { Layout };
static props = ["*"];
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.state = useState({
loading: true,
stats: {
total_students: 0,
total_teachers: 0,
total_courses: 0,
total_exams: 0,
active_sessions: 0,
pending_grading: 0,
pending_approval: 0,
total_entities: 0,
},
recentAttempts: [],
});
onWillStart(async () => {
await this.loadDashboardData();
});
}
async loadDashboardData() {
try {
const [students, teachers, courses, exams, sessions, pendingGrading, pendingApproval, entities] = await Promise.all([
this.orm.searchCount("res.users", [["account_source", "!=", false]]),
this.orm.searchCount("res.users", [["user_type", "=", "teacher"]]),
this.orm.searchCount("op.course", []),
this.orm.searchCount("encoach.exam.custom", []),
this.orm.searchCount("encoach.student.attempt", [["status", "=", "in_progress"]]),
this.orm.searchCount("encoach.student.attempt", [["status", "=", "scoring"]]),
this.orm.searchCount("encoach.student.attempt", [["status", "=", "pending_approval"]]),
this.orm.searchCount("encoach.entity", []),
]);
this.state.stats = {
total_students: students,
total_teachers: teachers,
total_courses: courses,
total_exams: exams,
active_sessions: sessions,
pending_grading: pendingGrading,
pending_approval: pendingApproval,
total_entities: entities,
};
const attempts = await this.orm.searchRead(
"encoach.student.attempt",
[],
["student_id", "exam_id", "status", "overall_band", "started_at"],
{ limit: 10, order: "started_at desc" },
);
this.state.recentAttempts = attempts;
} catch (e) {
console.error("Dashboard load error:", e);
} finally {
this.state.loading = false;
}
}
openAction(model, name, domain = []) {
this.action.doAction({
type: "ir.actions.act_window",
name: name,
res_model: model,
views: [[false, "list"], [false, "form"]],
domain: domain,
});
}
openStudents() { this.openAction("res.users", "Students", [["account_source", "!=", false]]); }
openTeachers() { this.openAction("res.users", "Teachers", [["user_type", "=", "teacher"]]); }
openCourses() { this.openAction("op.course", "Courses"); }
openExams() { this.openAction("encoach.exam.custom", "Exams"); }
openGradingQueue() { this.openAction("encoach.student.attempt", "Grading Queue", [["status", "=", "scoring"]]); }
openPendingApproval() { this.openAction("encoach.student.attempt", "Score Approval", [["status", "=", "pending_approval"]]); }
openEntities() { this.openAction("encoach.entity", "Entities"); }
}
registry.category("actions").add("encoach_dashboard", EncoachDashboard);

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="encoach_core.Dashboard">
<Layout display="{ controlPanel: {} }">
<div class="o_encoach_dashboard">
<div class="container-fluid py-4">
<h2 class="mb-4">EnCoach Dashboard</h2>
<t t-if="state.loading">
<div class="text-center py-5">
<i class="fa fa-spinner fa-spin fa-3x"/>
<p class="mt-2">Loading dashboard...</p>
</div>
</t>
<t t-else="">
<!-- KPI Cards -->
<div class="row g-3 mb-4">
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm h-100 cursor-pointer" t-on-click="openStudents">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary bg-opacity-10 p-3 me-3">
<i class="fa fa-users fa-2x text-primary"/>
</div>
<div>
<h3 class="mb-0" t-esc="state.stats.total_students"/>
<small class="text-muted">Students</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm h-100 cursor-pointer" t-on-click="openTeachers">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-success bg-opacity-10 p-3 me-3">
<i class="fa fa-chalkboard-teacher fa-2x text-success"/>
</div>
<div>
<h3 class="mb-0" t-esc="state.stats.total_teachers"/>
<small class="text-muted">Teachers</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm h-100 cursor-pointer" t-on-click="openCourses">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-info bg-opacity-10 p-3 me-3">
<i class="fa fa-book fa-2x text-info"/>
</div>
<div>
<h3 class="mb-0" t-esc="state.stats.total_courses"/>
<small class="text-muted">Courses</small>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="card shadow-sm h-100 cursor-pointer" t-on-click="openExams">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-warning bg-opacity-10 p-3 me-3">
<i class="fa fa-file-text fa-2x text-warning"/>
</div>
<div>
<h3 class="mb-0" t-esc="state.stats.total_exams"/>
<small class="text-muted">Exams</small>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Action Cards -->
<div class="row g-3 mb-4">
<div class="col-lg-4 col-md-6">
<div class="card shadow-sm border-start border-danger border-4 cursor-pointer" t-on-click="openGradingQueue">
<div class="card-body">
<h5>Grading Queue</h5>
<h2 class="text-danger" t-esc="state.stats.pending_grading"/>
<small class="text-muted">Exams awaiting grading</small>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="card shadow-sm border-start border-warning border-4 cursor-pointer" t-on-click="openPendingApproval">
<div class="card-body">
<h5>Pending Approval</h5>
<h2 class="text-warning" t-esc="state.stats.pending_approval"/>
<small class="text-muted">Scores awaiting release</small>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6">
<div class="card shadow-sm border-start border-info border-4 cursor-pointer" t-on-click="openEntities">
<div class="card-body">
<h5>Entities</h5>
<h2 class="text-info" t-esc="state.stats.total_entities"/>
<small class="text-muted">Registered organizations</small>
</div>
</div>
</div>
</div>
<!-- Recent Attempts Table -->
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0">Recent Exam Attempts</h5>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Student</th>
<th>Exam</th>
<th>Status</th>
<th>Band</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.recentAttempts" t-as="attempt" t-key="attempt.id">
<tr>
<td t-esc="attempt.student_id[1]"/>
<td t-esc="attempt.exam_id ? attempt.exam_id[1] : '-'"/>
<td>
<span class="badge"
t-att-class="{
'bg-success': attempt.status === 'released',
'bg-warning': attempt.status === 'pending_approval',
'bg-info': attempt.status === 'scoring',
'bg-secondary': attempt.status === 'in_progress',
'bg-primary': attempt.status === 'scored'
}"
t-esc="attempt.status"/>
</td>
<td t-esc="attempt.overall_band || '-'"/>
<td t-esc="attempt.started_at"/>
</tr>
</t>
<t t-if="!state.recentAttempts.length">
<tr>
<td colspan="5" class="text-center text-muted py-4">No recent attempts</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</t>
</div>
</div>
</Layout>
</t>
</templates>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="action_encoach_dashboard" model="ir.actions.client">
<field name="name">EnCoach Dashboard</field>
<field name="tag">encoach_dashboard</field>
</record>
<menuitem id="menu_encoach_dashboard_action"
name="Dashboard"
parent="menu_encoach_dashboard"
action="action_encoach_dashboard"
sequence="1"/>
</odoo>

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_encoach_entity_list" model="ir.ui.view">
<field name="name">encoach.entity.list</field>
<field name="model">encoach.entity</field>
<field name="arch" type="xml">
<list string="Entities">
<field name="name"/>
<field name="code"/>
<field name="type"/>
<field name="white_label_domain"/>
<field name="user_count"/>
<field name="results_release_mode"/>
<field name="active" column_invisible="True"/>
</list>
</field>
</record>
<record id="view_encoach_entity_form" model="ir.ui.view">
<field name="name">encoach.entity.form</field>
<field name="model">encoach.entity</field>
<field name="arch" type="xml">
<form string="Entity">
<sheet>
<div class="oe_button_box" name="button_box">
</div>
<field name="logo" widget="image" class="oe_avatar"/>
<div class="oe_title">
<h1><field name="name" placeholder="Entity Name"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="type"/>
<field name="active"/>
</group>
<group>
<field name="white_label_domain"/>
<field name="results_release_mode"/>
</group>
</group>
<notebook>
<page string="Branding">
<group>
<group>
<field name="primary_color" widget="color"/>
<field name="secondary_color" widget="color"/>
<field name="background_color" widget="color"/>
</group>
<group>
<field name="logo_url"/>
<field name="login_title"/>
<field name="favicon" widget="image"/>
</group>
</group>
<field name="login_description"/>
</page>
<page string="Users">
<field name="user_ids">
<list>
<field name="name"/>
<field name="login"/>
<field name="user_type"/>
</list>
</field>
</page>
<page string="Roles">
<field name="role_ids">
<list>
<field name="name"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_encoach_entity_search" model="ir.ui.view">
<field name="name">encoach.entity.search</field>
<field name="model">encoach.entity</field>
<field name="arch" type="xml">
<search string="Search Entities">
<field name="name"/>
<field name="code"/>
<filter string="Corporate" name="corporate" domain="[('type', '=', 'corporate')]"/>
<filter string="University" name="university" domain="[('type', '=', 'university')]"/>
<filter string="School" name="school" domain="[('type', '=', 'school')]"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<record id="action_encoach_entity" model="ir.actions.act_window">
<field name="name">Entities</field>
<field name="res_model">encoach.entity</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_encoach_entity_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first entity
</p>
<p>Entities represent organizations (corporates, universities, schools) on the EnCoach platform.</p>
</field>
</record>
<menuitem id="menu_encoach_entity_list"
name="Entities"
parent="menu_encoach_entity"
action="action_encoach_entity"
sequence="1"/>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="menu_encoach_root" name="EnCoach" sequence="10" web_icon="encoach_core,static/description/icon.png"/>
<menuitem id="menu_encoach_dashboard" name="Dashboard" parent="menu_encoach_root" sequence="1"/>
<menuitem id="menu_encoach_exams" name="Exams" parent="menu_encoach_root" sequence="10"/>
<menuitem id="menu_encoach_courses" name="Courses" parent="menu_encoach_root" sequence="20"/>
<menuitem id="menu_encoach_students" name="Students" parent="menu_encoach_root" sequence="30"/>
<menuitem id="menu_encoach_lms" name="LMS" parent="menu_encoach_root" sequence="40"/>
<menuitem id="menu_encoach_content" name="Content" parent="menu_encoach_root" sequence="50"/>
<menuitem id="menu_encoach_entity" name="Entity Management" parent="menu_encoach_root" sequence="60"/>
<menuitem id="menu_encoach_reports" name="Reports" parent="menu_encoach_root" sequence="70"/>
<menuitem id="menu_encoach_config" name="Configuration" parent="menu_encoach_root" sequence="80"/>
<menuitem id="menu_encoach_support" name="Support" parent="menu_encoach_root" sequence="90"/>
</odoo>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_entity_level_mapping_form" model="ir.ui.view">
<field name="name">encoach.entity.level.mapping.form</field>
<field name="model">encoach.entity.level.mapping</field>
<field name="arch" type="xml">
<form string="Entity Level Mapping">
<sheet>
<group>
<group>
<field name="entity_id"/>
<field name="internal_level_name"/>
<field name="cefr_equivalent"/>
</group>
<group>
<field name="min_score"/>
<field name="max_score"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_entity_level_mapping_list" model="ir.ui.view">
<field name="name">encoach.entity.level.mapping.list</field>
<field name="model">encoach.entity.level.mapping</field>
<field name="arch" type="xml">
<list string="Entity Level Mappings" editable="bottom">
<field name="entity_id"/>
<field name="internal_level_name"/>
<field name="min_score"/>
<field name="max_score"/>
<field name="cefr_equivalent"/>
</list>
</field>
</record>
<record id="action_entity_level_mapping" model="ir.actions.act_window">
<field name="name">Entity Level Mappings</field>
<field name="res_model">encoach.entity.level.mapping</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_entity_level_mapping"
name="Entity Level Mappings"
parent="encoach_core.menu_encoach_entity"
action="encoach_core.action_entity_level_mapping"
sequence="10"/>
</odoo>