first commit

This commit is contained in:
Suherdy Yacob 2026-05-21 17:04:01 +07:00
commit ccdc2f95df
9 changed files with 222 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@ -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

28
README.md Normal file
View File

@ -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.

1
__init__.py Normal file
View File

@ -0,0 +1 @@
from . import models

32
__manifest__.py Normal file
View File

@ -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',
}

2
models/__init__.py Normal file
View File

@ -0,0 +1,2 @@
from . import res_partner
from . import pos_order

9
models/pos_order.py Normal file
View File

@ -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)

69
models/res_partner.py Normal file
View File

@ -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

View File

@ -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);
}
});

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_pos_simplified" model="ir.ui.view">
<field name="name">res.partner.form.pos.simplified</field>
<field name="model">res.partner</field>
<field name="arch" type="xml">
<form string="Customer">
<sheet>
<group>
<group string="Personal Information">
<field name="name" required="1" placeholder="Customer Name"/>
<field name="phone" placeholder="Phone Number"/>
<field name="email" placeholder="Email Address"/>
<field name="birth_date" string="Birthday"/>
</group>
<group string="Address Information">
<label for="street" string="Address"/>
<div class="o_address_format">
<field name="street" placeholder="Street..." class="o_address_street"/>
<field name="street2" placeholder="Street 2..." class="o_address_street"/>
<field name="city" placeholder="City" class="o_address_city"/>
<field name="state_id" class="o_address_state" placeholder="State" options="{'no_open': True, 'no_quick_create': True}"/>
<field name="zip" placeholder="ZIP" class="o_address_zip"/>
<field name="country_id" placeholder="Country" class="o_address_country" options="{'no_open': True, 'no_create': True}"/>
</div>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="point_of_sale.res_partner_action_edit_pos" model="ir.actions.act_window">
<field name="view_id" ref="view_partner_form_pos_simplified"/>
</record>
</odoo>