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:
7
encoach_core/models/__init__.py
Normal file
7
encoach_core/models/__init__.py
Normal 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
|
||||
57
encoach_core/models/code.py
Normal file
57
encoach_core/models/code.py
Normal 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,
|
||||
}
|
||||
37
encoach_core/models/entity.py
Normal file
37
encoach_core/models/entity.py
Normal 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,
|
||||
}
|
||||
46
encoach_core/models/invite.py
Normal file
46
encoach_core/models/invite.py
Normal 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,
|
||||
}
|
||||
45
encoach_core/models/permission.py
Normal file
45
encoach_core/models/permission.py
Normal 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,
|
||||
}
|
||||
92
encoach_core/models/res_users.py
Normal file
92
encoach_core/models/res_users.py
Normal 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
|
||||
20
encoach_core/models/role.py
Normal file
20
encoach_core/models/role.py
Normal 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],
|
||||
}
|
||||
15
encoach_core/models/user_entity_rel.py
Normal file
15
encoach_core/models/user_entity_rel.py
Normal 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.",
|
||||
)
|
||||
Reference in New Issue
Block a user