feat: implement account activation, registration, and phone-based login with restricted portal access rules
This commit is contained in:
parent
5d188d8fec
commit
8dc4f61d74
@ -9,7 +9,7 @@
|
|||||||
'author': "Suherdy Yacob",
|
'author': "Suherdy Yacob",
|
||||||
'category': 'Marketing',
|
'category': 'Marketing',
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'depends': ['base', 'loyalty'],
|
'depends': ['base', 'loyalty', 'pos_loyalty', 'sale_loyalty'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/mapan_loyalty_push_security.xml',
|
'security/mapan_loyalty_push_security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
|||||||
@ -1,6 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
from odoo import http
|
from odoo import http
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
|
def normalize_phone_search(phone):
|
||||||
|
if not phone:
|
||||||
|
return []
|
||||||
|
digits = ''.join(c for c in phone if c.isdigit())
|
||||||
|
if not digits:
|
||||||
|
return []
|
||||||
|
candidates = [digits]
|
||||||
|
if digits.startswith('62'):
|
||||||
|
candidates.append('0' + digits[2:])
|
||||||
|
candidates.append(digits[2:])
|
||||||
|
elif digits.startswith('0'):
|
||||||
|
candidates.append('62' + digits[1:])
|
||||||
|
candidates.append(digits[1:])
|
||||||
|
else:
|
||||||
|
candidates.append('0' + digits)
|
||||||
|
candidates.append('62' + digits)
|
||||||
|
return list(set(candidates))
|
||||||
|
|
||||||
class AppNotificationController(http.Controller):
|
class AppNotificationController(http.Controller):
|
||||||
|
|
||||||
@http.route('/api/loyalty/fetch_notifications', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
@http.route('/api/loyalty/fetch_notifications', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
||||||
@ -47,3 +66,171 @@ class AppNotificationController(http.Controller):
|
|||||||
return {'status': 'success', 'data': branches}
|
return {'status': 'success', 'data': branches}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'status': 'error', 'message': str(e)}
|
return {'status': 'error', 'message': str(e)}
|
||||||
|
|
||||||
|
@http.route('/api/loyalty/activate_account', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
||||||
|
def activate_account(self, **kw):
|
||||||
|
"""
|
||||||
|
Public endpoint to activate/set password for an existing partner.
|
||||||
|
"""
|
||||||
|
phone = kw.get('phone')
|
||||||
|
password = kw.get('password')
|
||||||
|
birth_date = kw.get('birth_date')
|
||||||
|
|
||||||
|
if not phone or not password:
|
||||||
|
return {'status': 'error', 'message': 'Phone and Password are required.'}
|
||||||
|
|
||||||
|
candidates = normalize_phone_search(phone)
|
||||||
|
partner = request.env['res.partner'].sudo().search([
|
||||||
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not partner:
|
||||||
|
return {'status': 'error', 'message': 'Member with this phone number not found. Please contact support or register.'}
|
||||||
|
|
||||||
|
# Verify birth date if set on the partner
|
||||||
|
if partner.birth_date:
|
||||||
|
if not birth_date:
|
||||||
|
return {'status': 'error', 'message': 'Birth Date is required for verification.'}
|
||||||
|
if str(partner.birth_date) != str(birth_date):
|
||||||
|
return {'status': 'error', 'message': 'Birth Date does not match our records.'}
|
||||||
|
|
||||||
|
# Find or create user
|
||||||
|
user = request.env['res.users'].sudo().with_context(active_test=False).search([('partner_id', '=', partner.id)], limit=1)
|
||||||
|
if user:
|
||||||
|
user.sudo().write({
|
||||||
|
'company_id': request.env.company.id,
|
||||||
|
'company_ids': [(6, 0, [request.env.company.id])],
|
||||||
|
'password': password,
|
||||||
|
'active': True
|
||||||
|
})
|
||||||
|
portal_group = request.env.ref('base.group_portal')
|
||||||
|
if portal_group not in user.group_ids:
|
||||||
|
user.sudo().write({'group_ids': [(4, portal_group.id)]})
|
||||||
|
else:
|
||||||
|
portal_template = request.env.ref('base.template_portal_user_id')
|
||||||
|
existing_user = request.env['res.users'].sudo().with_context(active_test=False).search([('login', '=', phone)], limit=1)
|
||||||
|
if existing_user:
|
||||||
|
return {'status': 'error', 'message': 'A user with this phone number login already exists.'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
user = portal_template.sudo().with_context(no_reset_password=True).copy({
|
||||||
|
'name': partner.name,
|
||||||
|
'login': phone,
|
||||||
|
'email': partner.email or f"{phone}@miemapan.com",
|
||||||
|
'partner_id': partner.id,
|
||||||
|
'active': False,
|
||||||
|
})
|
||||||
|
# Write company, password and activate
|
||||||
|
user.sudo().write({
|
||||||
|
'company_id': request.env.company.id,
|
||||||
|
'company_ids': [(6, 0, [request.env.company.id])],
|
||||||
|
'password': password,
|
||||||
|
'active': True,
|
||||||
|
})
|
||||||
|
# Ensure portal group
|
||||||
|
portal_group = request.env.ref('base.group_portal')
|
||||||
|
if portal_group not in user.group_ids:
|
||||||
|
user.sudo().write({'group_ids': [(4, portal_group.id)]})
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': 'error', 'message': f'Failed to create user: {str(e)}'}
|
||||||
|
|
||||||
|
return {'status': 'success', 'message': 'Account activated successfully. You can now login.'}
|
||||||
|
|
||||||
|
@http.route('/api/loyalty/signup_member', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
||||||
|
def signup_member(self, **kw):
|
||||||
|
"""
|
||||||
|
Public endpoint to sign up a new member.
|
||||||
|
"""
|
||||||
|
name = kw.get('name')
|
||||||
|
phone = kw.get('phone')
|
||||||
|
birth_date = kw.get('birth_date')
|
||||||
|
gender = kw.get('gender')
|
||||||
|
password = kw.get('password')
|
||||||
|
|
||||||
|
if not name or not phone or not password:
|
||||||
|
return {'status': 'error', 'message': 'Name, Phone, and Password are required.'}
|
||||||
|
|
||||||
|
candidates = normalize_phone_search(phone)
|
||||||
|
|
||||||
|
existing_partner = request.env['res.partner'].sudo().search([
|
||||||
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
||||||
|
], limit=1)
|
||||||
|
if existing_partner:
|
||||||
|
return {'status': 'error', 'message': 'A member with this phone number is already registered.'}
|
||||||
|
|
||||||
|
existing_user = request.env['res.users'].sudo().with_context(active_test=False).search([
|
||||||
|
'|', ('login', '=', phone), ('login', 'in', candidates)
|
||||||
|
], limit=1)
|
||||||
|
if existing_user:
|
||||||
|
return {'status': 'error', 'message': 'A user with this phone number login already exists.'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
partner_vals = {
|
||||||
|
'name': name,
|
||||||
|
'phone': phone,
|
||||||
|
'birth_date': birth_date,
|
||||||
|
'gender': gender,
|
||||||
|
'company_id': request.env.company.id,
|
||||||
|
}
|
||||||
|
partner = request.env['res.partner'].sudo().create(partner_vals)
|
||||||
|
|
||||||
|
portal_template = request.env.ref('base.template_portal_user_id')
|
||||||
|
user = portal_template.sudo().with_context(no_reset_password=True).copy({
|
||||||
|
'name': partner.name,
|
||||||
|
'login': phone,
|
||||||
|
'email': f"{phone}@miemapan.com",
|
||||||
|
'partner_id': partner.id,
|
||||||
|
'active': False,
|
||||||
|
})
|
||||||
|
# Write company, password and activate
|
||||||
|
user.sudo().write({
|
||||||
|
'company_id': request.env.company.id,
|
||||||
|
'company_ids': [(6, 0, [request.env.company.id])],
|
||||||
|
'password': password,
|
||||||
|
'active': True,
|
||||||
|
'active': True,
|
||||||
|
})
|
||||||
|
# Ensure portal group
|
||||||
|
portal_group = request.env.ref('base.group_portal')
|
||||||
|
if portal_group not in user.group_ids:
|
||||||
|
user.sudo().write({'group_ids': [(4, portal_group.id)]})
|
||||||
|
|
||||||
|
return {'status': 'success', 'message': 'Account registered successfully.'}
|
||||||
|
except Exception as e:
|
||||||
|
return {'status': 'error', 'message': f'Registration failed: {str(e)}'}
|
||||||
|
|
||||||
|
@http.route('/api/loyalty/delete_account', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def delete_account(self, **kw):
|
||||||
|
"""
|
||||||
|
Authenticated endpoint to permanently delete/anonymize a member account.
|
||||||
|
"""
|
||||||
|
user = request.env.user
|
||||||
|
password = kw.get('password')
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
return {'status': 'error', 'message': 'Password is required.'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
user.with_user(user)._check_credentials({'type': 'password', 'password': password}, {'interactive': True})
|
||||||
|
except Exception:
|
||||||
|
return {'status': 'error', 'message': 'Incorrect password.'}
|
||||||
|
|
||||||
|
partner = user.partner_id
|
||||||
|
|
||||||
|
user.sudo().write({'active': False})
|
||||||
|
|
||||||
|
partner.sudo().write({
|
||||||
|
'name': f'Deleted Member {partner.id}',
|
||||||
|
'phone': False,
|
||||||
|
'email': False,
|
||||||
|
'birth_date': False,
|
||||||
|
'gender': False,
|
||||||
|
'active': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
partner.loyalty_card_ids.sudo().write({
|
||||||
|
'active': False,
|
||||||
|
'points': 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
return {'status': 'success', 'message': 'Account deleted successfully.'}
|
||||||
|
|||||||
@ -1,2 +1,5 @@
|
|||||||
from . import res_partner
|
from . import res_partner
|
||||||
from . import app_notification
|
from . import app_notification
|
||||||
|
from . import res_users
|
||||||
|
from . import loyalty_card
|
||||||
|
|
||||||
|
|||||||
13
models/loyalty_card.py
Normal file
13
models/loyalty_card.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
class LoyaltyCard(models.Model):
|
||||||
|
_inherit = 'loyalty.card'
|
||||||
|
|
||||||
|
def _compute_use_count(self):
|
||||||
|
# Perform computation as sudo to bypass access errors (like pos.order.line or sale.order.line)
|
||||||
|
# when accessed by portal/public users
|
||||||
|
sudo_self = self.sudo()
|
||||||
|
super(LoyaltyCard, sudo_self)._compute_use_count()
|
||||||
|
for card in self:
|
||||||
|
card.use_count = sudo_self.browse(card.id).use_count
|
||||||
36
models/res_users.py
Normal file
36
models/res_users.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.fields import Domain
|
||||||
|
|
||||||
|
class ResUsers(models.Model):
|
||||||
|
_inherit = 'res.users'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_login_domain(self, login):
|
||||||
|
domain = super()._get_login_domain(login)
|
||||||
|
if login and not self.env.context.get('in_get_login_domain'):
|
||||||
|
# Normalize digits of the login string
|
||||||
|
digits = ''.join(c for c in login if c.isdigit())
|
||||||
|
if digits:
|
||||||
|
candidates = [digits]
|
||||||
|
if digits.startswith('62'):
|
||||||
|
candidates.append('0' + digits[2:])
|
||||||
|
candidates.append(digits[2:])
|
||||||
|
elif digits.startswith('0'):
|
||||||
|
candidates.append('62' + digits[1:])
|
||||||
|
candidates.append(digits[1:])
|
||||||
|
else:
|
||||||
|
candidates.append('0' + digits)
|
||||||
|
candidates.append('62' + digits)
|
||||||
|
|
||||||
|
# Check for res.partner with this phone number
|
||||||
|
partners = self.env['res.partner'].with_context(in_get_login_domain=True).sudo().search([
|
||||||
|
'|', ('phone', 'in', candidates), ('phone_sanitized', 'in', candidates)
|
||||||
|
])
|
||||||
|
if partners:
|
||||||
|
users = self.with_context(in_get_login_domain=True).sudo().search([
|
||||||
|
('partner_id', 'in', partners.ids)
|
||||||
|
])
|
||||||
|
if users:
|
||||||
|
return Domain('id', 'in', users.ids)
|
||||||
|
return domain
|
||||||
@ -2,3 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|||||||
access_mapan_push_wizard_manager,mapan_push_wizard_manager,model_mapan_push_wizard,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
access_mapan_push_wizard_manager,mapan_push_wizard_manager,model_mapan_push_wizard,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
||||||
access_mapan_app_notification_user,mapan_app_notification_user,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_user,1,0,0,0
|
access_mapan_app_notification_user,mapan_app_notification_user,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_user,1,0,0,0
|
||||||
access_mapan_app_notification_manager,mapan_app_notification_manager,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
access_mapan_app_notification_manager,mapan_app_notification_manager,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
||||||
|
access_loyalty_card_portal,loyalty.card.portal,loyalty.model_loyalty_card,base.group_portal,1,0,0,0
|
||||||
|
access_loyalty_program_portal,loyalty.program.portal,loyalty.model_loyalty_program,base.group_portal,1,0,0,0
|
||||||
|
access_loyalty_history_portal,loyalty.history.portal,loyalty.model_loyalty_history,base.group_portal,1,0,0,0
|
||||||
|
access_loyalty_reward_portal,loyalty.reward.portal,loyalty.model_loyalty_reward,base.group_portal,1,0,0,0
|
||||||
|
access_loyalty_rule_portal,loyalty.rule.portal,loyalty.model_loyalty_rule,base.group_portal,1,0,0,0
|
||||||
|
|||||||
|
@ -22,4 +22,65 @@
|
|||||||
<field name="privilege_id" ref="res_groups_privilege_mapan_loyalty_push"/>
|
<field name="privilege_id" ref="res_groups_privilege_mapan_loyalty_push"/>
|
||||||
<field name="implied_ids" eval="[(4, ref('group_mapan_loyalty_push_user'))]"/>
|
<field name="implied_ids" eval="[(4, ref('group_mapan_loyalty_push_user'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- Record Rule for Portal Users to only view their own loyalty cards -->
|
||||||
|
<record id="loyalty_card_portal_rule" model="ir.rule">
|
||||||
|
<field name="name">Portal Loyalty Card: Own Cards Only</field>
|
||||||
|
<field name="model_id" ref="loyalty.model_loyalty_card"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||||
|
<field name="domain_force">[('partner_id', '=', user.partner_id.id)]</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Record Rule for Portal Users to only view history linked to their own loyalty cards -->
|
||||||
|
<record id="loyalty_history_portal_rule" model="ir.rule">
|
||||||
|
<field name="name">Portal Loyalty History: Own Cards Only</field>
|
||||||
|
<field name="model_id" ref="loyalty.model_loyalty_history"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||||
|
<field name="domain_force">[('card_id.partner_id', '=', user.partner_id.id)]</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Record Rule for Portal Users to view loyalty rewards in allowed companies -->
|
||||||
|
<record id="loyalty_reward_portal_rule" model="ir.rule">
|
||||||
|
<field name="name">Portal Loyalty Reward: Read Access</field>
|
||||||
|
<field name="model_id" ref="loyalty.model_loyalty_reward"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||||
|
<field name="domain_force">[('program_id.company_id', 'in', company_ids + [False])]</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Record Rule for Portal Users to view loyalty rules in allowed companies -->
|
||||||
|
<record id="loyalty_rule_portal_rule" model="ir.rule">
|
||||||
|
<field name="name">Portal Loyalty Rule: Read Access</field>
|
||||||
|
<field name="model_id" ref="loyalty.model_loyalty_rule"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||||
|
<field name="domain_force">[('program_id.company_id', 'in', company_ids + [False])]</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Record Rule for Portal Users to view loyalty programs in allowed companies -->
|
||||||
|
<record id="loyalty_program_portal_rule" model="ir.rule">
|
||||||
|
<field name="name">Portal Loyalty Program: Read Access</field>
|
||||||
|
<field name="model_id" ref="loyalty.model_loyalty_program"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||||
|
<field name="domain_force">[('company_id', 'in', company_ids + [False])]</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user