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])] + + + + + +