From ccdc2f95df623d930eb07f0921927739ffab9b6a Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Thu, 21 May 2026 17:04:01 +0700 Subject: [PATCH] first commit --- .gitignore | 33 ++++++++++ README.md | 28 ++++++++ __init__.py | 1 + __manifest__.py | 32 +++++++++ models/__init__.py | 2 + models/pos_order.py | 9 +++ models/res_partner.py | 69 ++++++++++++++++++++ static/src/app/screens/partner_list_patch.js | 12 ++++ views/res_partner_views.xml | 36 ++++++++++ 9 files changed, 222 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 models/__init__.py create mode 100644 models/pos_order.py create mode 100644 models/res_partner.py create mode 100644 static/src/app/screens/partner_list_patch.js create mode 100644 views/res_partner_views.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5537e00 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Editors and IDEs +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..8bc8f16 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# POS Loyalty Member Customization + +This custom Odoo module enhances the POS Loyalty interface and partner creation wizard, optimizing workflow efficiency and resolving critical multi-company operational constraints. + +## Features + +1. **Strict Customer Selection Filter:** + - Limits customer loading and search lists in the POS UI strictly to real individuals who hold an active membership or loyalty card (`is_loyalty_member = True`). + - Automatically excludes cashier accounts, user accounts, and company partners from being selected as customers. + +2. **Simplified Customer Creation Wizard:** + - Overrides the POS Customer Edit/Creation screen to show only essential details: + * Name + * Phone + * Email + * Address (Street, Zip, City, State, Country) + * Birthday + +3. **Automatic Lowest Level Membership Assignment:** + - Automatically provisions new partners with a loyalty card for the lowest membership tier (`Membership Silver`) immediately upon registration. + +4. **Robust Multi-Company Loyalty Processing:** + - Solves a core Odoo 19 multi-company operational bug where cashiers in branch companies encounter "Access Errors" when validating orders for customers whose loyalty cards belong to the parent company. + - Bypasses company-specific record rules during POS loyalty audit logging using standard and safe `sudo` environment execution. + +## Technical Details +- **JS OWL Patching:** Overrides POS frontend partner management safely using clean, standard owl-level class overrides. +- **Python Inheritance:** Customizes backend models (`res.partner`, `pos.order`) to clean loading domains and securely provision cross-company data. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..ceda515 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,32 @@ +{ + 'name': 'POS Loyalty Member Customizations', + 'version': '19.0.1.0.0', + 'summary': 'Limit customer search to loyalty members, simplify partner POS form, and auto-assign silver membership to new partners.', + 'description': """ +Custom POS Loyalty and Membership management features: +1. Limit customer search in POS UI to loyalty program members (who have a loyalty card). +2. Simplify new partner/edit partner wizard in POS to only require Name, Phone, Email, Address, and Birthday. +3. Automatically assign new customers to the lowest membership program level (Membership Silver) by creating a loyalty card. + """, + 'category': 'Sales/Point of Sale', + 'author': 'Suherdy Yacob', + 'depends': [ + 'base', + 'point_of_sale', + 'loyalty', + 'pos_loyalty', + 'res_partner_extended', + 'pos_loyalty_multi_level', + ], + 'data': [ + 'views/res_partner_views.xml', + ], + 'assets': { + 'point_of_sale._assets_pos': [ + 'pos_loyalty_member_custom/static/src/app/screens/partner_list_patch.js', + ] + }, + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e6d2229 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner +from . import pos_order diff --git a/models/pos_order.py b/models/pos_order.py new file mode 100644 index 0000000..2d049fa --- /dev/null +++ b/models/pos_order.py @@ -0,0 +1,9 @@ +from odoo import models + +class PosOrder(models.Model): + _inherit = 'pos.order' + + def add_loyalty_history_lines(self, coupon_data, coupon_updates): + # Override to execute with sudo() to bypass multi-company record rules + # when cashier in one company updates a card belonging to another company + return super(PosOrder, self.sudo()).add_loyalty_history_lines(coupon_data, coupon_updates) diff --git a/models/res_partner.py b/models/res_partner.py new file mode 100644 index 0000000..a39c409 --- /dev/null +++ b/models/res_partner.py @@ -0,0 +1,69 @@ +from odoo import models, fields, api + +class ResPartner(models.Model): + _inherit = 'res.partner' + + loyalty_card_ids = fields.One2many( + 'loyalty.card', + 'partner_id', + string='Loyalty Cards' + ) + + is_loyalty_member = fields.Boolean( + string='Is Loyalty Member', + compute='_compute_is_loyalty_member', + ) + + @api.depends('loyalty_card_ids') + def _compute_is_loyalty_member(self): + for partner in self: + partner.is_loyalty_member = bool(partner.loyalty_card_ids) + + @api.model + def _load_pos_data_domain(self, data, config): + domain = super()._load_pos_data_domain(data, config) + # OR together the standard POS loaded partners (cashier, active orders) with all individual loyalty members + return ['|'] + domain + ['&', ('is_company', '=', False), ('loyalty_card_ids', '!=', False)] + + @api.model + def _load_pos_data_fields(self, config): + fields = super()._load_pos_data_fields(config) + if 'is_loyalty_member' not in fields: + fields.append('is_loyalty_member') + return fields + + @api.model + def get_new_partner(self, config_id, domain, offset): + # Limit active searches strictly to individual customers who are loyalty members + domain.extend([('is_company', '=', False), ('loyalty_card_ids', '!=', False)]) + return super().get_new_partner(config_id, domain, offset) + + @api.model_create_multi + def create(self, vals_list): + partners = super().create(vals_list) + for partner in partners: + if partner.is_company: + continue + + # Find the lowest membership program level (Membership Silver) + lowest_program = self.env['loyalty.program'].sudo().search( + [('multi_level_membership', '=', True)], + order='minimum_spend asc', + limit=1 + ) + if lowest_program: + existing_card = self.env['loyalty.card'].sudo().search([ + ('partner_id', '=', partner.id), + ('program_id', '=', lowest_program.id) + ], limit=1) + if not existing_card: + self.env['loyalty.card'].sudo().create({ + 'partner_id': partner.id, + 'program_id': lowest_program.id, + 'points': 0, + }) + + if hasattr(partner, 'membership_level_id') and not partner.membership_level_id: + partner.sudo().write({'membership_level_id': lowest_program.id}) + + return partners diff --git a/static/src/app/screens/partner_list_patch.js b/static/src/app/screens/partner_list_patch.js new file mode 100644 index 0000000..1584c7b --- /dev/null +++ b/static/src/app/screens/partner_list_patch.js @@ -0,0 +1,12 @@ +/** @odoo-module **/ + +import { PartnerList } from "@point_of_sale/app/screens/partner_list/partner_list"; +import { patch } from "@web/core/utils/patch"; + +patch(PartnerList.prototype, { + getPartners(partners) { + // Filter the partner list to strictly only display partners who are loyalty members + const filteredPartners = partners.filter((p) => p.is_loyalty_member); + return super.getPartners(filteredPartners); + } +}); diff --git a/views/res_partner_views.xml b/views/res_partner_views.xml new file mode 100644 index 0000000..47239b4 --- /dev/null +++ b/views/res_partner_views.xml @@ -0,0 +1,36 @@ + + + + res.partner.form.pos.simplified + res.partner + +
+ + + + + + + + + + + + +
+
+
+ + + + +