diff --git a/__manifest__.py b/__manifest__.py
index abebd69..24c4dac 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -9,7 +9,7 @@
'author': "Suherdy Yacob",
'category': 'Marketing',
'version': '1.0',
- 'depends': ['base', 'loyalty'],
+ 'depends': ['base', 'loyalty', 'pos_loyalty', 'sale_loyalty'],
'data': [
'security/mapan_loyalty_push_security.xml',
'security/ir.model.access.csv',
diff --git a/controllers/main.py b/controllers/main.py
index 5db93f4..d8243a4 100644
--- a/controllers/main.py
+++ b/controllers/main.py
@@ -1,6 +1,25 @@
+# -*- coding: utf-8 -*-
from odoo import http
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):
@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}
except Exception as 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.'}
diff --git a/models/__init__.py b/models/__init__.py
index b09769f..85f12c8 100644
--- a/models/__init__.py
+++ b/models/__init__.py
@@ -1,2 +1,5 @@
from . import res_partner
from . import app_notification
+from . import res_users
+from . import loyalty_card
+
diff --git a/models/loyalty_card.py b/models/loyalty_card.py
new file mode 100644
index 0000000..161ceb9
--- /dev/null
+++ b/models/loyalty_card.py
@@ -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
diff --git a/models/res_users.py b/models/res_users.py
new file mode 100644
index 0000000..2979c97
--- /dev/null
+++ b/models/res_users.py
@@ -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
diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv
index b4e00f5..156b5c8 100644
--- a/security/ir.model.access.csv
+++ b/security/ir.model.access.csv
@@ -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_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_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
diff --git a/security/mapan_loyalty_push_security.xml b/security/mapan_loyalty_push_security.xml
index 5d57339..e340c72 100644
--- a/security/mapan_loyalty_push_security.xml
+++ b/security/mapan_loyalty_push_security.xml
@@ -22,4 +22,65 @@
+
+
+
+ Portal Loyalty Card: Own Cards Only
+
+
+ [('partner_id', '=', user.partner_id.id)]
+
+
+
+
+
+
+
+
+ Portal Loyalty History: Own Cards Only
+
+
+ [('card_id.partner_id', '=', user.partner_id.id)]
+
+
+
+
+
+
+
+
+ Portal Loyalty Reward: Read Access
+
+
+ [('program_id.company_id', 'in', company_ids + [False])]
+
+
+
+
+
+
+
+
+ Portal Loyalty Rule: Read Access
+
+
+ [('program_id.company_id', 'in', company_ids + [False])]
+
+
+
+
+
+
+
+
+ Portal Loyalty Program: Read Access
+
+
+ [('company_id', 'in', company_ids + [False])]
+
+
+
+
+
+