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

View File

@@ -0,0 +1,7 @@
from . import res_users
from . import entity
from . import role
from . import permission
from . import code
from . import invite
from . import user_entity_rel

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,37 @@
from odoo import models, fields
class EncoachEntity(models.Model):
_name = "encoach.entity"
_description = "EnCoach Entity (Organization)"
_inherit = ["mail.thread"]
name = fields.Char(required=True, tracking=True)
label = fields.Char()
licenses = fields.Integer(default=0)
expiry_date = fields.Datetime()
payment_currency = fields.Char()
payment_price = fields.Float(digits=(12, 2))
role_ids = fields.One2many("encoach.role", "entity_id", string="Roles")
user_rel_ids = fields.One2many(
"encoach.user.entity.rel", "entity_id", string="Members",
)
legacy_id = fields.Char(index=True)
def to_encoach_dict(self):
self.ensure_one()
payment = None
if self.payment_currency and self.payment_price:
payment = {
"currency": self.payment_currency,
"price": self.payment_price,
}
return {
"id": self.id,
"label": self.label or self.name,
"licenses": self.licenses,
"expiryDate": (
self.expiry_date.isoformat() if self.expiry_date else None
),
"payment": payment,
}

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,45 @@
from odoo import models, fields
PERMISSION_TOPICS = [
("exam", "Exam Management"),
("user", "User Management"),
("entity", "Entity Management"),
("group", "Group Management"),
("assignment", "Assignment Management"),
("code", "Code Management"),
("invite", "Invite Management"),
("payment", "Payment Management"),
("ticket", "Ticket Management"),
("approval", "Approval Workflow"),
("stats", "Statistics"),
("training", "Training"),
]
PERMISSION_TYPES = [
("create", "Create"),
("read", "Read"),
("update", "Update"),
("delete", "Delete"),
]
class EncoachPermission(models.Model):
_name = "encoach.permission"
_description = "EnCoach Permission"
topic = fields.Selection(PERMISSION_TOPICS, required=True)
perm_type = fields.Selection(PERMISSION_TYPES, required=True)
legacy_id = fields.Char(index=True)
_topic_type_unique = models.Constraint(
"UNIQUE(topic, perm_type)",
"Permission topic/type combination must be unique.",
)
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"topic": self.topic,
"type": self.perm_type,
}

View File

@@ -0,0 +1,92 @@
from odoo import models, fields, api
USER_TYPES = [
("student", "Student"),
("teacher", "Teacher"),
("corporate", "Corporate"),
("admin", "Admin"),
("developer", "Developer"),
("agent", "Agent"),
("mastercorporate", "Master Corporate"),
]
DEFAULT_LEVELS = {"reading": 0, "listening": 0, "writing": 0, "speaking": 0}
class ResUsers(models.Model):
_inherit = "res.users"
encoach_type = fields.Selection(
USER_TYPES, string="EnCoach User Type", default="student",
)
focus = fields.Char()
levels = fields.Json(default=DEFAULT_LEVELS)
desired_levels = fields.Json(default=DEFAULT_LEVELS)
is_verified = fields.Boolean(default=False)
subscription_expiration = fields.Datetime()
registration_date = fields.Datetime(default=fields.Datetime.now)
bio = fields.Text()
profile_picture = fields.Char()
is_first_login = fields.Boolean(default=True)
last_login = fields.Datetime()
encoach_status = fields.Selection(
[("active", "Active"), ("inactive", "Inactive"), ("suspended", "Suspended")],
string="EnCoach Status",
default="active",
)
entity_rel_ids = fields.One2many(
"encoach.user.entity.rel", "user_id", string="Entity Memberships",
)
legacy_id = fields.Char(index=True)
def to_encoach_dict(self):
"""Serialize to the JSON shape the frontend expects."""
self.ensure_one()
entities = []
for rel in self.entity_rel_ids:
entities.append({
"id": rel.entity_id.id,
"role": rel.role_id.name if rel.role_id else None,
})
return {
"id": self.id,
"email": self.login,
"name": self.name,
"profilePicture": self.profile_picture or "",
"isFirstLogin": self.is_first_login,
"focus": self.focus or "",
"levels": self.levels or DEFAULT_LEVELS,
"desiredLevels": self.desired_levels or DEFAULT_LEVELS,
"type": self.encoach_type,
"bio": self.bio or "",
"isVerified": self.is_verified,
"subscriptionExpirationDate": (
self.subscription_expiration.isoformat()
if self.subscription_expiration
else None
),
"registrationDate": (
self.registration_date.isoformat()
if self.registration_date
else None
),
"status": self.encoach_status,
"lastLogin": (
self.last_login.isoformat() if self.last_login else None
),
"entities": entities,
"permissions": self._get_encoach_permissions(),
}
def _get_encoach_permissions(self):
self.ensure_one()
perms = {}
for rel in self.entity_rel_ids:
if rel.role_id:
for perm in rel.role_id.permission_ids:
key = perm.topic
if key not in perms:
perms[key] = []
if perm.perm_type not in perms[key]:
perms[key].append(perm.perm_type)
return perms

View File

@@ -0,0 +1,20 @@
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")
legacy_id = fields.Char(index=True)
def to_encoach_dict(self):
self.ensure_one()
return {
"id": self.id,
"name": self.name,
"entity": self.entity_id.id,
"permissions": [p.to_encoach_dict() for p in self.permission_ids],
}

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.",
)