first commit
This commit is contained in:
commit
ccdc2f95df
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal 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
28
README.md
Normal 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
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
32
__manifest__.py
Normal file
32
__manifest__.py
Normal 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
2
models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import res_partner
|
||||
from . import pos_order
|
||||
9
models/pos_order.py
Normal file
9
models/pos_order.py
Normal 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
69
models/res_partner.py
Normal 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
|
||||
12
static/src/app/screens/partner_list_patch.js
Normal file
12
static/src/app/screens/partner_list_patch.js
Normal 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);
|
||||
}
|
||||
});
|
||||
36
views/res_partner_views.xml
Normal file
36
views/res_partner_views.xml
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user