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:
1
encoach_core/__init__.py
Normal file
1
encoach_core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
25
encoach_core/__manifest__.py
Normal file
25
encoach_core/__manifest__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "EnCoach Core",
|
||||
"version": "19.0.1.0.0",
|
||||
"category": "Education",
|
||||
"summary": "Core models for the EnCoach IELTS platform",
|
||||
"description": "User type extensions, entity management, roles, permissions, codes, invites.",
|
||||
"depends": ["base", "mail"],
|
||||
"data": [
|
||||
"security/security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"data/constants.xml",
|
||||
"views/menu.xml",
|
||||
"views/entity_views.xml",
|
||||
"views/role_views.xml",
|
||||
"views/permission_views.xml",
|
||||
"views/code_views.xml",
|
||||
"views/invite_views.xml",
|
||||
"views/user_entity_rel_views.xml",
|
||||
"views/res_users_views.xml",
|
||||
"data/assign_admin_group.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"application": True,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
8
encoach_core/data/assign_admin_group.xml
Normal file
8
encoach_core/data/assign_admin_group.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="base.user_admin" model="res.users" context="{'no_reset_password': True}">
|
||||
<field name="group_ids" eval="[
|
||||
(4, ref('encoach_core.group_encoach_developer')),
|
||||
]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
56
encoach_core/data/constants.xml
Normal file
56
encoach_core/data/constants.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- JWT signing secret -->
|
||||
<record id="param_jwt_secret" model="ir.config_parameter">
|
||||
<field name="key">encoach.jwt_secret</field>
|
||||
<field name="value">CHANGE_ME_IN_PRODUCTION_RANDOM_256_BIT_STRING</field>
|
||||
</record>
|
||||
<record id="param_jwt_expiry_hours" model="ir.config_parameter">
|
||||
<field name="key">encoach.jwt_expiry_hours</field>
|
||||
<field name="value">72</field>
|
||||
</record>
|
||||
|
||||
<!-- AI service keys (placeholders) -->
|
||||
<record id="param_openai_key" model="ir.config_parameter">
|
||||
<field name="key">encoach.openai_api_key</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_aws_access_key" model="ir.config_parameter">
|
||||
<field name="key">encoach.aws_access_key_id</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_aws_secret_key" model="ir.config_parameter">
|
||||
<field name="key">encoach.aws_secret_access_key</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_elai_token" model="ir.config_parameter">
|
||||
<field name="key">encoach.elai_token</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_gptzero_key" model="ir.config_parameter">
|
||||
<field name="key">encoach.gptzero_api_key</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_stripe_secret" model="ir.config_parameter">
|
||||
<field name="key">encoach.stripe_secret_key</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_stripe_webhook" model="ir.config_parameter">
|
||||
<field name="key">encoach.stripe_webhook_secret</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_paypal_client" model="ir.config_parameter">
|
||||
<field name="key">encoach.paypal_client_id</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_paypal_secret" model="ir.config_parameter">
|
||||
<field name="key">encoach.paypal_client_secret</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
<record id="param_paymob_key" model="ir.config_parameter">
|
||||
<field name="key">encoach.paymob_api_key</field>
|
||||
<field name="value"></field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
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.",
|
||||
)
|
||||
69
encoach_core/scripts/import_avatars.py
Normal file
69
encoach_core/scripts/import_avatars.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Import ELAI avatars from avatars.json into encoach.elai.avatar.
|
||||
|
||||
Usage (Odoo shell):
|
||||
./odoo-bin shell -c odoo.conf -d encoach_prod
|
||||
>>> exec(open('../addons_extra/encoach_core/scripts/import_avatars.py').read())
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
_logger = logging.getLogger("encoach.avatar_import")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
|
||||
AVATARS_JSON = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "encoach", "encoach_backend", "elai", "avatars.json"
|
||||
)
|
||||
|
||||
|
||||
def import_avatars(env, json_path=None):
|
||||
path = json_path or AVATARS_JSON
|
||||
if not os.path.exists(path):
|
||||
_logger.error("avatars.json not found at %s", path)
|
||||
return
|
||||
|
||||
with open(path, "r") as f:
|
||||
avatars = json.load(f)
|
||||
|
||||
if isinstance(avatars, dict):
|
||||
avatars = list(avatars.values())
|
||||
|
||||
created = 0
|
||||
skipped = 0
|
||||
|
||||
for av in avatars:
|
||||
code = av.get("code", av.get("avatar_code", ""))
|
||||
if not code:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
existing = env["encoach.elai.avatar"].sudo().search([("avatar_code", "=", code)], limit=1)
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
voice = av.get("voice", {})
|
||||
voice_id = voice.get("voiceId") if isinstance(voice, dict) else av.get("voice_id")
|
||||
voice_provider = voice.get("voiceProvider") if isinstance(voice, dict) else av.get("voice_provider")
|
||||
|
||||
env["encoach.elai.avatar"].sudo().create({
|
||||
"name": av.get("name", code),
|
||||
"avatar_code": code,
|
||||
"avatar_url": av.get("url", av.get("avatar_url", "")),
|
||||
"gender": av.get("gender"),
|
||||
"canvas": av.get("canvas"),
|
||||
"voice_id": voice_id,
|
||||
"voice_provider": voice_provider,
|
||||
})
|
||||
created += 1
|
||||
|
||||
_logger.info("Avatar import complete: created=%d, skipped=%d", created, skipped)
|
||||
|
||||
|
||||
try:
|
||||
env # noqa: F821
|
||||
import_avatars(env)
|
||||
except NameError:
|
||||
_logger.info("Run this script inside Odoo shell")
|
||||
98
encoach_core/scripts/migrate_firebase_auth.py
Normal file
98
encoach_core/scripts/migrate_firebase_auth.py
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Firebase Auth -> Odoo res.users migration script.
|
||||
|
||||
Exports users from Firebase Auth (via Admin SDK) and creates/updates
|
||||
corresponding Odoo res.users records with EnCoach extensions.
|
||||
|
||||
Usage (Odoo shell):
|
||||
./odoo-bin shell -c odoo.conf -d encoach_prod
|
||||
>>> exec(open('../addons_extra/encoach_core/scripts/migrate_firebase_auth.py').read())
|
||||
|
||||
Requires: firebase-admin
|
||||
pip install firebase-admin
|
||||
"""
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger("encoach.firebase_migration")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
|
||||
try:
|
||||
import firebase_admin
|
||||
from firebase_admin import auth as fb_auth, credentials
|
||||
except ImportError:
|
||||
_logger.error("firebase-admin is required: pip install firebase-admin")
|
||||
firebase_admin = None
|
||||
|
||||
|
||||
SERVICE_ACCOUNT_PATH = "serviceAccountKey.json"
|
||||
|
||||
|
||||
def migrate_firebase_users(env, service_account_path=SERVICE_ACCOUNT_PATH):
|
||||
if not firebase_admin:
|
||||
_logger.error("firebase-admin not installed")
|
||||
return
|
||||
|
||||
cred = credentials.Certificate(service_account_path)
|
||||
try:
|
||||
firebase_admin.get_app()
|
||||
except ValueError:
|
||||
firebase_admin.initialize_app(cred)
|
||||
|
||||
_logger.info("Fetching Firebase users...")
|
||||
|
||||
created = 0
|
||||
updated = 0
|
||||
skipped = 0
|
||||
errors = 0
|
||||
|
||||
page = fb_auth.list_users()
|
||||
while page:
|
||||
for fb_user in page.users:
|
||||
email = fb_user.email
|
||||
if not email:
|
||||
skipped += 1
|
||||
continue
|
||||
try:
|
||||
existing = env["res.users"].sudo().search([("login", "=", email)], limit=1)
|
||||
if existing:
|
||||
vals = {}
|
||||
if fb_user.display_name and not existing.name:
|
||||
vals["name"] = fb_user.display_name
|
||||
if fb_user.email_verified and not existing.is_verified:
|
||||
vals["is_verified"] = True
|
||||
if fb_user.photo_url and not existing.profile_picture:
|
||||
vals["profile_picture"] = fb_user.photo_url
|
||||
if vals:
|
||||
existing.sudo().write(vals)
|
||||
updated += 1
|
||||
else:
|
||||
skipped += 1
|
||||
else:
|
||||
env["res.users"].sudo().create({
|
||||
"login": email,
|
||||
"name": fb_user.display_name or email.split("@")[0],
|
||||
"password": "changeme_firebase_migrated",
|
||||
"encoach_type": "student",
|
||||
"is_verified": fb_user.email_verified,
|
||||
"profile_picture": fb_user.photo_url or "",
|
||||
"legacy_id": fb_user.uid,
|
||||
})
|
||||
created += 1
|
||||
except Exception as e:
|
||||
_logger.error("Error migrating %s: %s", email, e)
|
||||
errors += 1
|
||||
|
||||
page = page.get_next_page()
|
||||
|
||||
_logger.info(
|
||||
"Firebase migration complete: created=%d, updated=%d, skipped=%d, errors=%d",
|
||||
created, updated, skipped, errors,
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
env # noqa: F821
|
||||
migrate_firebase_users(env)
|
||||
except NameError:
|
||||
_logger.info("Run this script inside Odoo shell")
|
||||
731
encoach_core/scripts/migrate_mongodb.py
Normal file
731
encoach_core/scripts/migrate_mongodb.py
Normal file
@@ -0,0 +1,731 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MongoDB -> Odoo PostgreSQL data migration script.
|
||||
|
||||
Usage (from Odoo shell):
|
||||
./odoo-bin shell -c odoo.conf -d encoach_prod
|
||||
>>> exec(open('../addons_extra/encoach_core/scripts/migrate_mongodb.py').read())
|
||||
|
||||
Or standalone:
|
||||
python migrate_mongodb.py --mongo-uri mongodb://localhost:27017 --mongo-db encoach --odoo-url http://localhost:8069 --odoo-db encoach_prod
|
||||
|
||||
Requires: pymongo, xmlrpc.client (stdlib)
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
_logger = logging.getLogger("encoach.migration")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
|
||||
try:
|
||||
from pymongo import MongoClient
|
||||
except ImportError:
|
||||
_logger.error("pymongo is required: pip install pymongo")
|
||||
sys.exit(1)
|
||||
|
||||
COLLECTION_MAP = {
|
||||
"users": "res.users",
|
||||
"entities": "encoach.entity",
|
||||
"roles": "encoach.role",
|
||||
"permissions": "encoach.permission",
|
||||
"codes": "encoach.code",
|
||||
"invites": "encoach.invite",
|
||||
"exams": "encoach.exam",
|
||||
"groups": "encoach.group",
|
||||
"assignments": "encoach.assignment",
|
||||
"sessions": "encoach.session",
|
||||
"stats": "encoach.stat",
|
||||
"evaluations": "encoach.evaluation",
|
||||
"trainings": "encoach.training",
|
||||
"tips": "encoach.training.tip",
|
||||
"walkthroughs": "encoach.walkthrough",
|
||||
"packages": "encoach.package",
|
||||
"payments": "encoach.subscription.payment",
|
||||
"discounts": "encoach.discount",
|
||||
"tickets": "encoach.ticket",
|
||||
"avatars": "encoach.elai.avatar",
|
||||
}
|
||||
|
||||
LEGACY_ID_CACHE = {}
|
||||
|
||||
|
||||
def _oid(doc, key="_id"):
|
||||
val = doc.get(key)
|
||||
if val is None:
|
||||
return None
|
||||
return str(val)
|
||||
|
||||
|
||||
def _dt(val):
|
||||
if isinstance(val, datetime):
|
||||
return val.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if isinstance(val, str):
|
||||
return val
|
||||
if isinstance(val, (int, float)):
|
||||
return datetime.fromtimestamp(val / 1000).strftime("%Y-%m-%d %H:%M:%S")
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_legacy(model, legacy_id, env):
|
||||
"""Look up Odoo record ID by legacy MongoDB _id."""
|
||||
if not legacy_id:
|
||||
return False
|
||||
cache_key = (model, str(legacy_id))
|
||||
if cache_key in LEGACY_ID_CACHE:
|
||||
return LEGACY_ID_CACHE[cache_key]
|
||||
rec = env[model].sudo().search([("legacy_id", "=", str(legacy_id))], limit=1)
|
||||
if rec:
|
||||
LEGACY_ID_CACHE[cache_key] = rec.id
|
||||
return rec.id
|
||||
return False
|
||||
|
||||
|
||||
class EncoachMigrator:
|
||||
|
||||
def __init__(self, mongo_uri, mongo_db, env):
|
||||
self.client = MongoClient(mongo_uri)
|
||||
self.db = self.client[mongo_db]
|
||||
self.env = env
|
||||
self.stats = {k: {"total": 0, "created": 0, "skipped": 0, "error": 0} for k in COLLECTION_MAP}
|
||||
|
||||
def run(self):
|
||||
_logger.info("Starting EnCoach data migration")
|
||||
self._migrate_entities()
|
||||
self._migrate_permissions()
|
||||
self._migrate_roles()
|
||||
self._migrate_users()
|
||||
self._migrate_codes()
|
||||
self._migrate_invites()
|
||||
self._migrate_exams()
|
||||
self._migrate_groups()
|
||||
self._migrate_assignments()
|
||||
self._migrate_sessions()
|
||||
self._migrate_stats()
|
||||
self._migrate_evaluations()
|
||||
self._migrate_trainings()
|
||||
self._migrate_tips()
|
||||
self._migrate_walkthroughs()
|
||||
self._migrate_packages()
|
||||
self._migrate_payments()
|
||||
self._migrate_discounts()
|
||||
self._migrate_tickets()
|
||||
self._migrate_avatars()
|
||||
self._print_summary()
|
||||
|
||||
def _migrate_entities(self):
|
||||
coll = "entities"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.entity"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
payment = doc.get("payment", {}) or {}
|
||||
rec = self.env["encoach.entity"].sudo().create({
|
||||
"name": doc.get("label", doc.get("name", "Unknown")),
|
||||
"label": doc.get("label"),
|
||||
"licenses": doc.get("licenses", 0),
|
||||
"expiry_date": _dt(doc.get("expiryDate")),
|
||||
"payment_currency": payment.get("currency"),
|
||||
"payment_price": payment.get("price", 0),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.entity", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Entity %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_permissions(self):
|
||||
coll = "permissions"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.permission"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
rec = self.env["encoach.permission"].sudo().create({
|
||||
"topic": doc.get("topic", "exam"),
|
||||
"perm_type": doc.get("type", "read"),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.permission", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Permission %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_roles(self):
|
||||
coll = "roles"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.role"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
entity_id = _resolve_legacy("encoach.entity", doc.get("entity"), self.env)
|
||||
perm_ids = []
|
||||
for pid in doc.get("permissions", []):
|
||||
odoo_id = _resolve_legacy("encoach.permission", str(pid), self.env)
|
||||
if odoo_id:
|
||||
perm_ids.append(odoo_id)
|
||||
rec = self.env["encoach.role"].sudo().create({
|
||||
"name": doc.get("name", "Unknown"),
|
||||
"entity_id": entity_id or False,
|
||||
"permission_ids": [(6, 0, perm_ids)],
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.role", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Role %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_users(self):
|
||||
coll = "users"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
email = doc.get("email", "")
|
||||
if not email:
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
existing = self.env["res.users"].sudo().search([
|
||||
"|", ("login", "=", email), ("legacy_id", "=", lid),
|
||||
], limit=1)
|
||||
if existing:
|
||||
if not existing.legacy_id:
|
||||
existing.sudo().write({"legacy_id": lid})
|
||||
LEGACY_ID_CACHE[("res.users", lid)] = existing.id
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
vals = {
|
||||
"login": email,
|
||||
"name": doc.get("name", email.split("@")[0]),
|
||||
"password": "changeme_migrated",
|
||||
"encoach_type": doc.get("type", "student"),
|
||||
"focus": doc.get("focus"),
|
||||
"levels": doc.get("levels"),
|
||||
"desired_levels": doc.get("desiredLevels"),
|
||||
"is_verified": doc.get("isVerified", False),
|
||||
"bio": doc.get("bio"),
|
||||
"profile_picture": doc.get("profilePicture"),
|
||||
"registration_date": _dt(doc.get("registrationDate")),
|
||||
"subscription_expiration": _dt(doc.get("subscriptionExpirationDate")),
|
||||
"legacy_id": lid,
|
||||
}
|
||||
rec = self.env["res.users"].sudo().create(vals)
|
||||
LEGACY_ID_CACHE[("res.users", lid)] = rec.id
|
||||
|
||||
for ent_info in doc.get("entities", []):
|
||||
ent_id = ent_info if isinstance(ent_info, str) else ent_info.get("id") if isinstance(ent_info, dict) else str(ent_info)
|
||||
entity_odoo = _resolve_legacy("encoach.entity", ent_id, self.env)
|
||||
if entity_odoo:
|
||||
role_id = None
|
||||
if isinstance(ent_info, dict) and ent_info.get("role"):
|
||||
role_id = _resolve_legacy("encoach.role", ent_info["role"], self.env)
|
||||
self.env["encoach.user.entity.rel"].sudo().create({
|
||||
"user_id": rec.id,
|
||||
"entity_id": entity_odoo,
|
||||
"role_id": role_id or False,
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("User %s (%s): %s", lid, email, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_codes(self):
|
||||
coll = "codes"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.code"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
creator_id = _resolve_legacy("res.users", doc.get("creator"), self.env)
|
||||
entity_id = _resolve_legacy("encoach.entity", doc.get("entity"), self.env)
|
||||
rec = self.env["encoach.code"].sudo().create({
|
||||
"code": doc.get("code", lid[:8]),
|
||||
"entity_id": entity_id or False,
|
||||
"creator_id": creator_id or self.env.ref("base.user_admin").id,
|
||||
"expiry_date": _dt(doc.get("expiryDate")),
|
||||
"code_type": doc.get("type", "individual"),
|
||||
"max_uses": doc.get("maxUses", 1),
|
||||
"uses": doc.get("uses", 0),
|
||||
"user_type": doc.get("userType", "student"),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.code", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Code %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_invites(self):
|
||||
coll = "invites"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.invite"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
from_id = _resolve_legacy("res.users", doc.get("from"), self.env)
|
||||
to_id = _resolve_legacy("res.users", doc.get("to"), self.env)
|
||||
entity_id = _resolve_legacy("encoach.entity", doc.get("entity"), self.env)
|
||||
if not (from_id and to_id and entity_id):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
rec = self.env["encoach.invite"].sudo().create({
|
||||
"from_user_id": from_id,
|
||||
"to_user_id": to_id,
|
||||
"entity_id": entity_id,
|
||||
"status": doc.get("status", "pending"),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.invite", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Invite %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_exams(self):
|
||||
coll = "exams"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.exam"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
module = doc.get("module", "reading")
|
||||
owner_ids = []
|
||||
for oid in doc.get("owners", []):
|
||||
uid = _resolve_legacy("res.users", str(oid), self.env)
|
||||
if uid:
|
||||
owner_ids.append(uid)
|
||||
entity_ids = []
|
||||
for eid in doc.get("entities", []):
|
||||
e_id = _resolve_legacy("encoach.entity", str(eid), self.env)
|
||||
if e_id:
|
||||
entity_ids.append(e_id)
|
||||
rec = self.env["encoach.exam"].sudo().create({
|
||||
"module": module,
|
||||
"min_timer": doc.get("minTimer", 0),
|
||||
"is_diagnostic": doc.get("isDiagnostic", False),
|
||||
"variant": doc.get("variant"),
|
||||
"difficulty": doc.get("difficulty"),
|
||||
"owner_ids": [(6, 0, owner_ids)],
|
||||
"entity_ids": [(6, 0, entity_ids)],
|
||||
"shuffle": doc.get("shuffle", False),
|
||||
"access": doc.get("access", "public"),
|
||||
"label": doc.get("label", ""),
|
||||
"requires_approval": doc.get("requiresApproval", False),
|
||||
"approved": doc.get("approved", False),
|
||||
"parts": doc.get("parts", []),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.exam", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Exam %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_groups(self):
|
||||
coll = "groups"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.group"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
admin_id = _resolve_legacy("res.users", doc.get("admin"), self.env)
|
||||
participant_ids = []
|
||||
for pid in doc.get("participants", []):
|
||||
uid = _resolve_legacy("res.users", str(pid), self.env)
|
||||
if uid:
|
||||
participant_ids.append(uid)
|
||||
entity_id = _resolve_legacy("encoach.entity", doc.get("entity"), self.env)
|
||||
rec = self.env["encoach.group"].sudo().create({
|
||||
"name": doc.get("name", "Unnamed"),
|
||||
"admin_id": admin_id or self.env.ref("base.user_admin").id,
|
||||
"participant_ids": [(6, 0, participant_ids)],
|
||||
"disable_editing": doc.get("disableEditing", False),
|
||||
"entity_id": entity_id or False,
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.group", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Group %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_assignments(self):
|
||||
coll = "assignments"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.assignment"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
assigner_id = _resolve_legacy("res.users", doc.get("assigner"), self.env)
|
||||
assignee_ids = [
|
||||
_resolve_legacy("res.users", str(a), self.env)
|
||||
for a in doc.get("assignees", [])
|
||||
]
|
||||
assignee_ids = [a for a in assignee_ids if a]
|
||||
exam_ids = [
|
||||
_resolve_legacy("encoach.exam", str(e), self.env)
|
||||
for e in doc.get("exams", [])
|
||||
]
|
||||
exam_ids = [e for e in exam_ids if e]
|
||||
teacher_ids = [
|
||||
_resolve_legacy("res.users", str(t), self.env)
|
||||
for t in doc.get("teachers", [])
|
||||
]
|
||||
teacher_ids = [t for t in teacher_ids if t]
|
||||
entity_id = _resolve_legacy("encoach.entity", doc.get("entity"), self.env)
|
||||
rec = self.env["encoach.assignment"].sudo().create({
|
||||
"name": doc.get("name", "Unnamed"),
|
||||
"assigner_id": assigner_id or self.env.ref("base.user_admin").id,
|
||||
"assignee_ids": [(6, 0, assignee_ids)],
|
||||
"exam_ids": [(6, 0, exam_ids)],
|
||||
"results": doc.get("results"),
|
||||
"instructor_gender": doc.get("instructorGender"),
|
||||
"start_date": _dt(doc.get("startDate")),
|
||||
"end_date": _dt(doc.get("endDate")),
|
||||
"teacher_ids": [(6, 0, teacher_ids)],
|
||||
"archived": doc.get("archived", False),
|
||||
"released": doc.get("released", False),
|
||||
"started": doc.get("start", False),
|
||||
"auto_start": doc.get("autoStart", False),
|
||||
"entity_id": entity_id or False,
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.assignment", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Assignment %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_sessions(self):
|
||||
coll = "sessions"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.session"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
user_id = _resolve_legacy("res.users", doc.get("user"), self.env)
|
||||
if not user_id:
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
exam_data = {k: v for k, v in doc.items() if k not in ("_id", "user", "__v")}
|
||||
rec = self.env["encoach.session"].sudo().create({
|
||||
"user_id": user_id,
|
||||
"exam_data": exam_data,
|
||||
"date": _dt(doc.get("date")) or _dt(doc.get("_id").generation_time) if hasattr(doc.get("_id"), "generation_time") else None,
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.session", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Session %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_stats(self):
|
||||
coll = "stats"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.stat"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
user_id = _resolve_legacy("res.users", doc.get("user"), self.env)
|
||||
if not user_id:
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
exam_id = _resolve_legacy("encoach.exam", doc.get("exam"), self.env)
|
||||
session_id = _resolve_legacy("encoach.session", doc.get("session"), self.env)
|
||||
assignment_id = _resolve_legacy("encoach.assignment", doc.get("assignment"), self.env)
|
||||
score = doc.get("score", {})
|
||||
rec = self.env["encoach.stat"].sudo().create({
|
||||
"user_id": user_id,
|
||||
"exam_id": exam_id or False,
|
||||
"exercise": doc.get("exercise"),
|
||||
"session_id": session_id or False,
|
||||
"date": _dt(doc.get("date")),
|
||||
"module": doc.get("module"),
|
||||
"solutions": doc.get("solutions"),
|
||||
"stat_type": doc.get("type"),
|
||||
"time_spent": doc.get("timeSpent", 0),
|
||||
"inactivity": doc.get("inactivity", 0),
|
||||
"assignment_id": assignment_id or False,
|
||||
"score_correct": score.get("correct", 0) if isinstance(score, dict) else 0,
|
||||
"score_total": score.get("total", 0) if isinstance(score, dict) else 0,
|
||||
"score_missing": score.get("missing", 0) if isinstance(score, dict) else 0,
|
||||
"is_disabled": doc.get("isDisabled", False),
|
||||
"shuffle_maps": doc.get("shuffleMaps"),
|
||||
"is_practice": doc.get("isPractice", False),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.stat", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Stat %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_evaluations(self):
|
||||
coll = "evaluations"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.evaluation"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
user_id = _resolve_legacy("res.users", doc.get("user"), self.env)
|
||||
rec = self.env["encoach.evaluation"].sudo().create({
|
||||
"user_id": user_id or False,
|
||||
"exercise_id": doc.get("exerciseId"),
|
||||
"exercise_type": doc.get("exerciseType"),
|
||||
"status": doc.get("status", "completed"),
|
||||
"result": doc.get("result"),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.evaluation", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Evaluation %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_trainings(self):
|
||||
coll = "trainings"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.training"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
user_id = _resolve_legacy("res.users", doc.get("user"), self.env)
|
||||
rec = self.env["encoach.training"].sudo().create({
|
||||
"user_id": user_id or self.env.ref("base.user_admin").id,
|
||||
"exams": doc.get("exams"),
|
||||
"tips": doc.get("tips"),
|
||||
"weak_areas": doc.get("weakAreas"),
|
||||
"legacy_id": lid,
|
||||
})
|
||||
LEGACY_ID_CACHE[("encoach.training", lid)] = rec.id
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Training %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_tips(self):
|
||||
coll = "tips"
|
||||
_logger.info("Migrating %s (from training.tip collection)...", coll)
|
||||
for doc in self.db.get_collection("training_tips").find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.training.tip"].sudo().search([("tip_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
self.env["encoach.training.tip"].sudo().create({
|
||||
"tip_id": lid,
|
||||
"category": doc.get("category"),
|
||||
"content": doc.get("content", ""),
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Tip %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_walkthroughs(self):
|
||||
coll = "walkthroughs"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
try:
|
||||
self.env["encoach.walkthrough"].sudo().create({
|
||||
"name": doc.get("name", ""),
|
||||
"content": doc.get("content"),
|
||||
"module": doc.get("module"),
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Walkthrough: %s", e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_packages(self):
|
||||
coll = "packages"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
try:
|
||||
self.env["encoach.package"].sudo().create({
|
||||
"name": doc.get("name", "Package"),
|
||||
"price": doc.get("price", 0),
|
||||
"currency": doc.get("currency", "USD"),
|
||||
"duration_days": doc.get("duration", 30),
|
||||
"features": doc.get("features"),
|
||||
"active": doc.get("active", True),
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Package: %s", e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_payments(self):
|
||||
coll = "payments"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
try:
|
||||
user_id = _resolve_legacy("res.users", doc.get("user"), self.env)
|
||||
entity_id = _resolve_legacy("encoach.entity", doc.get("entity"), self.env)
|
||||
self.env["encoach.subscription.payment"].sudo().create({
|
||||
"user_id": user_id or False,
|
||||
"provider": doc.get("provider", "stripe"),
|
||||
"status": doc.get("status", "completed"),
|
||||
"amount": doc.get("amount", 0),
|
||||
"currency": doc.get("currency", "USD"),
|
||||
"transaction_id": doc.get("transactionId"),
|
||||
"entity_id": entity_id or False,
|
||||
"legacy_id": lid,
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Payment %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_discounts(self):
|
||||
coll = "discounts"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
try:
|
||||
self.env["encoach.discount"].sudo().create({
|
||||
"code": doc.get("code", ""),
|
||||
"percentage": doc.get("percentage", 0),
|
||||
"expiry_date": _dt(doc.get("expiryDate")),
|
||||
"max_uses": doc.get("maxUses", 0),
|
||||
"uses": doc.get("uses", 0),
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Discount: %s", e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_tickets(self):
|
||||
coll = "tickets"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db[coll].find():
|
||||
self.stats[coll]["total"] += 1
|
||||
lid = _oid(doc)
|
||||
if self.env["encoach.ticket"].sudo().search([("legacy_id", "=", lid)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
reporter = doc.get("reporter", {})
|
||||
assigned_id = _resolve_legacy("res.users", doc.get("assignedTo"), self.env)
|
||||
self.env["encoach.ticket"].sudo().create({
|
||||
"reporter_name": reporter.get("name") if isinstance(reporter, dict) else None,
|
||||
"reporter_email": reporter.get("email") if isinstance(reporter, dict) else None,
|
||||
"subject": doc.get("subject", ""),
|
||||
"description": doc.get("description", ""),
|
||||
"ticket_type": doc.get("type", "help"),
|
||||
"status": doc.get("status", "open"),
|
||||
"exam_information": doc.get("examInformation"),
|
||||
"assigned_to_id": assigned_id or False,
|
||||
"legacy_id": lid,
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Ticket %s: %s", lid, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _migrate_avatars(self):
|
||||
coll = "avatars"
|
||||
_logger.info("Migrating %s...", coll)
|
||||
for doc in self.db.get_collection("avatars").find():
|
||||
self.stats[coll]["total"] += 1
|
||||
code = doc.get("code", doc.get("avatar_code", ""))
|
||||
if self.env["encoach.elai.avatar"].sudo().search([("avatar_code", "=", code)], limit=1):
|
||||
self.stats[coll]["skipped"] += 1
|
||||
continue
|
||||
try:
|
||||
self.env["encoach.elai.avatar"].sudo().create({
|
||||
"name": doc.get("name", code),
|
||||
"avatar_code": code,
|
||||
"avatar_url": doc.get("url", doc.get("avatar_url", "")),
|
||||
"gender": doc.get("gender"),
|
||||
"canvas": doc.get("canvas"),
|
||||
"voice_id": doc.get("voice", {}).get("voiceId") if isinstance(doc.get("voice"), dict) else doc.get("voice_id"),
|
||||
"voice_provider": doc.get("voice", {}).get("voiceProvider") if isinstance(doc.get("voice"), dict) else doc.get("voice_provider"),
|
||||
})
|
||||
self.stats[coll]["created"] += 1
|
||||
except Exception as e:
|
||||
_logger.error("Avatar %s: %s", code, e)
|
||||
self.stats[coll]["error"] += 1
|
||||
|
||||
def _print_summary(self):
|
||||
_logger.info("\n========== MIGRATION SUMMARY ==========")
|
||||
total_created = 0
|
||||
total_skipped = 0
|
||||
total_errors = 0
|
||||
for coll, s in self.stats.items():
|
||||
_logger.info(
|
||||
" %-15s total=%4d created=%4d skipped=%4d errors=%4d",
|
||||
coll, s["total"], s["created"], s["skipped"], s["error"],
|
||||
)
|
||||
total_created += s["created"]
|
||||
total_skipped += s["skipped"]
|
||||
total_errors += s["error"]
|
||||
_logger.info(" %-15s total=%4d created=%4d skipped=%4d errors=%4d",
|
||||
"TOTAL", total_created + total_skipped + total_errors,
|
||||
total_created, total_skipped, total_errors)
|
||||
_logger.info("========================================\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Migrate EnCoach MongoDB to Odoo")
|
||||
parser.add_argument("--mongo-uri", default="mongodb://localhost:27017")
|
||||
parser.add_argument("--mongo-db", default="encoach")
|
||||
args = parser.parse_args()
|
||||
_logger.info("Standalone mode -- connect to Odoo via xmlrpc or run inside Odoo shell")
|
||||
_logger.info("Example: ./odoo-bin shell -c odoo.conf -d encoach_prod")
|
||||
_logger.info("Then: exec(open('path/to/migrate_mongodb.py').read())")
|
||||
else:
|
||||
try:
|
||||
env # noqa: F821 -- available in Odoo shell
|
||||
migrator = EncoachMigrator("mongodb://localhost:27017", "encoach", env)
|
||||
migrator.run()
|
||||
except NameError:
|
||||
pass
|
||||
90
encoach_core/scripts/rebuild_faiss.py
Normal file
90
encoach_core/scripts/rebuild_faiss.py
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Rebuild FAISS indices for EnCoach training tips.
|
||||
|
||||
After data migration, this script re-embeds all training tips using
|
||||
sentence-transformers and builds per-category FAISS indices.
|
||||
|
||||
Usage (Odoo shell):
|
||||
./odoo-bin shell -c odoo.conf -d encoach_prod
|
||||
>>> exec(open('../addons_extra/encoach_core/scripts/rebuild_faiss.py').read())
|
||||
|
||||
Requires: faiss-cpu, sentence-transformers
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
|
||||
_logger = logging.getLogger("encoach.faiss_rebuild")
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||
|
||||
try:
|
||||
import faiss
|
||||
from sentence_transformers import SentenceTransformer
|
||||
except ImportError:
|
||||
_logger.error("Install: pip install faiss-cpu sentence-transformers")
|
||||
faiss = None
|
||||
|
||||
MODEL_NAME = "sentence-transformers/all-MiniLM-L6-v2"
|
||||
EMBEDDING_DIM = 384
|
||||
FAISS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "encoach_training", "data", "faiss")
|
||||
|
||||
CATEGORIES = [
|
||||
"ct_focus",
|
||||
"language_for_writing",
|
||||
"reading_skill",
|
||||
"strategy",
|
||||
"writing_skill",
|
||||
"word_link",
|
||||
"word_partners",
|
||||
]
|
||||
|
||||
|
||||
def rebuild_faiss_indices(env, output_dir=None):
|
||||
if not faiss:
|
||||
return
|
||||
|
||||
output_dir = output_dir or FAISS_DIR
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
_logger.info("Loading sentence-transformers model: %s", MODEL_NAME)
|
||||
model = SentenceTransformer(MODEL_NAME)
|
||||
|
||||
for category in CATEGORIES:
|
||||
tips = env["encoach.training.tip"].sudo().search([("category", "=", category)])
|
||||
if not tips:
|
||||
_logger.info(" %s: no tips, skipping", category)
|
||||
continue
|
||||
|
||||
contents = [t.content for t in tips]
|
||||
tip_ids = [t.tip_id for t in tips]
|
||||
_logger.info(" %s: encoding %d tips...", category, len(contents))
|
||||
|
||||
embeddings = model.encode(contents, show_progress_bar=False, convert_to_numpy=True)
|
||||
embeddings = embeddings.astype(np.float32)
|
||||
|
||||
for tip, emb in zip(tips, embeddings):
|
||||
tip.sudo().write({"embedding": pickle.dumps(emb)})
|
||||
|
||||
index = faiss.IndexFlatL2(EMBEDDING_DIM)
|
||||
index.add(embeddings)
|
||||
|
||||
index_path = os.path.join(output_dir, f"{category}.index")
|
||||
faiss.write_index(index, index_path)
|
||||
|
||||
ids_path = os.path.join(output_dir, f"{category}_ids.pkl")
|
||||
with open(ids_path, "wb") as f:
|
||||
pickle.dump(tip_ids, f)
|
||||
|
||||
_logger.info(" %s: indexed %d tips -> %s", category, len(tips), index_path)
|
||||
|
||||
_logger.info("FAISS rebuild complete. Indices saved to %s", output_dir)
|
||||
|
||||
|
||||
try:
|
||||
env # noqa: F821
|
||||
rebuild_faiss_indices(env)
|
||||
except NameError:
|
||||
_logger.info("Run this script inside Odoo shell")
|
||||
21
encoach_core/security/ir.model.access.csv
Normal file
21
encoach_core/security/ir.model.access.csv
Normal file
@@ -0,0 +1,21 @@
|
||||
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_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_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_admin,encoach.permission.admin,model_encoach_permission,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,1,0
|
||||
access_encoach_code_admin,encoach.code.admin,model_encoach_code,group_encoach_admin,1,1,1,1
|
||||
access_encoach_invite_student,encoach.invite.student,model_encoach_invite,group_encoach_student,1,1,0,0
|
||||
access_encoach_invite_teacher,encoach.invite.teacher,model_encoach_invite,group_encoach_teacher,1,1,1,0
|
||||
access_encoach_invite_admin,encoach.invite.admin,model_encoach_invite,group_encoach_admin,1,1,1,1
|
||||
access_encoach_user_entity_rel_student,encoach.user.entity.rel.student,model_encoach_user_entity_rel,group_encoach_student,1,0,0,0
|
||||
access_encoach_user_entity_rel_admin,encoach.user.entity.rel.admin,model_encoach_user_entity_rel,group_encoach_admin,1,1,1,1
|
||||
access_encoach_entity_sysadmin,encoach.entity.sysadmin,model_encoach_entity,base.group_system,1,1,1,1
|
||||
access_encoach_role_sysadmin,encoach.role.sysadmin,model_encoach_role,base.group_system,1,1,1,1
|
||||
access_encoach_permission_sysadmin,encoach.permission.sysadmin,model_encoach_permission,base.group_system,1,1,1,1
|
||||
access_encoach_code_sysadmin,encoach.code.sysadmin,model_encoach_code,base.group_system,1,1,1,1
|
||||
access_encoach_invite_sysadmin,encoach.invite.sysadmin,model_encoach_invite,base.group_system,1,1,1,1
|
||||
access_encoach_user_entity_rel_sysadmin,encoach.user.entity.rel.sysadmin,model_encoach_user_entity_rel,base.group_system,1,1,1,1
|
||||
|
30
encoach_core/security/security.xml
Normal file
30
encoach_core/security/security.xml
Normal 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>
|
||||
65
encoach_core/views/code_views.xml
Normal file
65
encoach_core/views/code_views.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_code_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.code.tree</field>
|
||||
<field name="model">encoach.code</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Registration Codes" editable="bottom">
|
||||
<field name="code"/>
|
||||
<field name="entity_id"/>
|
||||
<field name="creator_id"/>
|
||||
<field name="code_type"/>
|
||||
<field name="user_type"/>
|
||||
<field name="max_uses"/>
|
||||
<field name="uses"/>
|
||||
<field name="expiry_date"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_code_form" model="ir.ui.view">
|
||||
<field name="name">encoach.code.form</field>
|
||||
<field name="model">encoach.code</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Registration Code">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="code" readonly="id"/>
|
||||
<field name="entity_id"/>
|
||||
<field name="creator_id"/>
|
||||
<field name="expiry_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="code_type"/>
|
||||
<field name="user_type"/>
|
||||
<field name="max_uses"/>
|
||||
<field name="uses"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_code" model="ir.actions.act_window">
|
||||
<field name="name">Registration Codes</field>
|
||||
<field name="res_model">encoach.code</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first registration code.
|
||||
</p>
|
||||
<p>
|
||||
Registration codes allow users to join entities or register
|
||||
with a specific user type.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_code"
|
||||
name="Registration Codes"
|
||||
parent="encoach_core.menu_encoach_users"
|
||||
action="action_encoach_code"
|
||||
sequence="20"/>
|
||||
</odoo>
|
||||
82
encoach_core/views/entity_views.xml
Normal file
82
encoach_core/views/entity_views.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_entity_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.entity.tree</field>
|
||||
<field name="model">encoach.entity</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Entities" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="label"/>
|
||||
<field name="licenses"/>
|
||||
<field name="expiry_date"/>
|
||||
<field name="payment_currency"/>
|
||||
<field name="payment_price"/>
|
||||
</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>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="label"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="licenses"/>
|
||||
<field name="expiry_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="payment_currency"/>
|
||||
<field name="payment_price"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Roles" name="roles">
|
||||
<field name="role_ids">
|
||||
<list editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="permission_ids" widget="many2many_tags"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Members" name="members">
|
||||
<field name="user_rel_ids">
|
||||
<list editable="bottom">
|
||||
<field name="user_id"/>
|
||||
<field name="role_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<chatter/>
|
||||
</form>
|
||||
</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="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first entity (organization).
|
||||
</p>
|
||||
<p>
|
||||
Entities represent organizations such as schools or companies.
|
||||
Each entity has licenses, members, and roles.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_entity"
|
||||
name="Entities"
|
||||
parent="encoach_core.menu_encoach_users"
|
||||
action="action_encoach_entity"
|
||||
sequence="10"/>
|
||||
</odoo>
|
||||
59
encoach_core/views/invite_views.xml
Normal file
59
encoach_core/views/invite_views.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_invite_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.invite.tree</field>
|
||||
<field name="model">encoach.invite</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Invites" editable="bottom">
|
||||
<field name="from_user_id"/>
|
||||
<field name="to_user_id"/>
|
||||
<field name="entity_id"/>
|
||||
<field name="status"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_invite_form" model="ir.ui.view">
|
||||
<field name="name">encoach.invite.form</field>
|
||||
<field name="model">encoach.invite</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Invite">
|
||||
<header>
|
||||
<button name="action_accept" string="Accept" type="object"
|
||||
class="btn-primary" invisible="status != 'pending'"/>
|
||||
<button name="action_decline" string="Decline" type="object"
|
||||
invisible="status != 'pending'"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="from_user_id"/>
|
||||
<field name="to_user_id"/>
|
||||
<field name="entity_id"/>
|
||||
<field name="status"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_invite" model="ir.actions.act_window">
|
||||
<field name="name">Invites</field>
|
||||
<field name="res_model">encoach.invite</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No invites yet.
|
||||
</p>
|
||||
<p>
|
||||
Invites allow users to join entities.
|
||||
Pending invites can be accepted or declined.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_invite"
|
||||
name="Invites"
|
||||
parent="encoach_core.menu_encoach_users"
|
||||
action="action_encoach_invite"
|
||||
sequence="30"/>
|
||||
</odoo>
|
||||
13
encoach_core/views/menu.xml
Normal file
13
encoach_core/views/menu.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_encoach_root" name="EnCoach" sequence="100"/>
|
||||
|
||||
<menuitem id="menu_encoach_users" name="Users & Entities" parent="menu_encoach_root" sequence="10"/>
|
||||
<menuitem id="menu_encoach_exams" name="Exams" parent="menu_encoach_root" sequence="20"/>
|
||||
<menuitem id="menu_encoach_classroom" name="Classroom" parent="menu_encoach_root" sequence="30"/>
|
||||
<menuitem id="menu_encoach_ai" name="AI & Grading" parent="menu_encoach_root" sequence="40"/>
|
||||
<menuitem id="menu_encoach_training" name="Training" parent="menu_encoach_root" sequence="50"/>
|
||||
<menuitem id="menu_encoach_subscription" name="Subscriptions" parent="menu_encoach_root" sequence="60"/>
|
||||
<menuitem id="menu_encoach_support" name="Support" parent="menu_encoach_root" sequence="70"/>
|
||||
<menuitem id="menu_encoach_config" name="Configuration" parent="menu_encoach_root" sequence="90"/>
|
||||
</odoo>
|
||||
49
encoach_core/views/permission_views.xml
Normal file
49
encoach_core/views/permission_views.xml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_permission_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.permission.tree</field>
|
||||
<field name="model">encoach.permission</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Permissions" editable="bottom">
|
||||
<field name="topic"/>
|
||||
<field name="perm_type"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_permission_form" model="ir.ui.view">
|
||||
<field name="name">encoach.permission.form</field>
|
||||
<field name="model">encoach.permission</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Permission">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="topic"/>
|
||||
<field name="perm_type"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_permission" model="ir.actions.act_window">
|
||||
<field name="name">Permissions</field>
|
||||
<field name="res_model">encoach.permission</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first permission.
|
||||
</p>
|
||||
<p>
|
||||
Permissions define what actions can be performed on each topic.
|
||||
Assign permissions to roles.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_permission"
|
||||
name="Permissions"
|
||||
parent="encoach_core.menu_encoach_config"
|
||||
action="action_encoach_permission"
|
||||
sequence="20"/>
|
||||
</odoo>
|
||||
39
encoach_core/views/res_users_views.xml
Normal file
39
encoach_core/views/res_users_views.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_res_users_form_encoach" model="ir.ui.view">
|
||||
<field name="name">res.users.form.encoach</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="EnCoach" name="encoach">
|
||||
<group>
|
||||
<group>
|
||||
<field name="encoach_type"/>
|
||||
<field name="focus"/>
|
||||
<field name="levels" widget="json"/>
|
||||
<field name="desired_levels" widget="json"/>
|
||||
<field name="is_verified"/>
|
||||
<field name="subscription_expiration"/>
|
||||
<field name="registration_date"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="bio"/>
|
||||
<field name="profile_picture"/>
|
||||
<field name="encoach_status"/>
|
||||
<field name="is_first_login"/>
|
||||
<field name="last_login"/>
|
||||
<field name="legacy_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="entity_rel_ids">
|
||||
<list editable="bottom">
|
||||
<field name="entity_id"/>
|
||||
<field name="role_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
50
encoach_core/views/role_views.xml
Normal file
50
encoach_core/views/role_views.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_role_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.role.tree</field>
|
||||
<field name="model">encoach.role</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Roles" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="entity_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_role_form" model="ir.ui.view">
|
||||
<field name="name">encoach.role.form</field>
|
||||
<field name="model">encoach.role</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Role">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="entity_id"/>
|
||||
<field name="permission_ids" widget="many2many_checkboxes"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_role" model="ir.actions.act_window">
|
||||
<field name="name">Roles</field>
|
||||
<field name="res_model">encoach.role</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first role.
|
||||
</p>
|
||||
<p>
|
||||
Roles define permissions within an entity.
|
||||
Assign permissions to roles to control what users can do.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_role"
|
||||
name="Roles"
|
||||
parent="encoach_core.menu_encoach_config"
|
||||
action="action_encoach_role"
|
||||
sequence="10"/>
|
||||
</odoo>
|
||||
50
encoach_core/views/user_entity_rel_views.xml
Normal file
50
encoach_core/views/user_entity_rel_views.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_user_entity_rel_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.user.entity.rel.tree</field>
|
||||
<field name="model">encoach.user.entity.rel</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="User-Entity Memberships" editable="bottom">
|
||||
<field name="user_id"/>
|
||||
<field name="entity_id"/>
|
||||
<field name="role_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_user_entity_rel_form" model="ir.ui.view">
|
||||
<field name="name">encoach.user.entity.rel.form</field>
|
||||
<field name="model">encoach.user.entity.rel</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="User-Entity Membership">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="user_id"/>
|
||||
<field name="entity_id"/>
|
||||
<field name="role_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_user_entity_rel" model="ir.actions.act_window">
|
||||
<field name="name">User-Entity Memberships</field>
|
||||
<field name="res_model">encoach.user.entity.rel</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No memberships yet.
|
||||
</p>
|
||||
<p>
|
||||
User-entity memberships link users to entities with optional roles.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_user_entity_rel"
|
||||
name="Memberships"
|
||||
parent="encoach_core.menu_encoach_users"
|
||||
action="action_encoach_user_entity_rel"
|
||||
sequence="40"/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user