first commit
This commit is contained in:
commit
e2b70ae161
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.DS_Store
|
||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
24
__manifest__.py
Normal file
24
__manifest__.py
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
'name': 'POS Hide Margin',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Point of Sale',
|
||||
'summary': 'Hide margin and cost fields in POS frontend and backend views for selected users',
|
||||
'description': """
|
||||
Adds a configuration on the User form (Hide Margin).
|
||||
When active:
|
||||
- All POS margin/cost fields are hidden in form, tree, search and pivot views.
|
||||
- POS frontend popup hides cost and margin details.
|
||||
""",
|
||||
'author': 'Suherdy Yacob',
|
||||
'depends': ['point_of_sale'],
|
||||
'data': [
|
||||
'views/res_users_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'point_of_sale._assets_pos': [
|
||||
'pos_hide_margin/static/src/app/**/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
2
models/__init__.py
Normal file
2
models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import res_users
|
||||
from . import pos_order
|
||||
210
models/pos_order.py
Normal file
210
models/pos_order.py
Normal file
@ -0,0 +1,210 @@
|
||||
from lxml import etree
|
||||
from odoo import models, api, fields
|
||||
|
||||
|
||||
def _hide_margin_fields_in_view(env, res):
|
||||
if env.user.x_hide_margin:
|
||||
for view_type, view_data in res.get('views', {}).items():
|
||||
arch = view_data.get('arch')
|
||||
if arch:
|
||||
doc = etree.fromstring(arch)
|
||||
# Hide field elements matching margin or margin_percent
|
||||
for field_node in doc.xpath("//field[@name='margin' or @name='margin_percent']"):
|
||||
field_node.set('invisible', '1')
|
||||
field_node.set('column_invisible', 'True')
|
||||
# Hide label elements matching margin or margin_percent
|
||||
for label_node in doc.xpath("//label[@for='margin' or @for='margin_percent']"):
|
||||
label_node.set('invisible', '1')
|
||||
# Hide the container div matching field margin
|
||||
for div_node in doc.xpath("//div[field[@name='margin']]"):
|
||||
div_node.set('invisible', '1')
|
||||
# Remove measure fields in pivot/graph views
|
||||
for measure_node in doc.xpath("//field[@name='margin' and @type='measure']"):
|
||||
measure_node.getparent().remove(measure_node)
|
||||
# Hide filters/groupby on margin or margin_percent in search views
|
||||
for filter_node in doc.xpath("//filter[@name='margin' or @name='margin_percent' or contains(@context, 'margin') or contains(@domain, 'margin')]"):
|
||||
filter_node.getparent().remove(filter_node)
|
||||
view_data['arch'] = etree.tostring(doc, encoding='utf-8', xml_declaration=False).decode('utf-8')
|
||||
return res
|
||||
|
||||
|
||||
def _hide_margin_fields_in_fields_get(env, res):
|
||||
if env.user.x_hide_margin:
|
||||
for field in ['margin', 'margin_percent']:
|
||||
if field in res:
|
||||
res[field]['invisible'] = True
|
||||
res[field]['searchable'] = False
|
||||
res[field]['sortable'] = False
|
||||
return res
|
||||
|
||||
|
||||
def _hide_margin_fields_in_read_group_dict(env, res):
|
||||
if env.user.x_hide_margin:
|
||||
for group in res:
|
||||
for key in list(group.keys()):
|
||||
if 'margin' in key:
|
||||
group[key] = 0.0
|
||||
return res
|
||||
|
||||
|
||||
def _hide_margin_fields_in_read_group(env, groupby, aggregates, res):
|
||||
if env.user.x_hide_margin:
|
||||
margin_indices = [
|
||||
i for i, agg in enumerate(aggregates)
|
||||
if 'margin' in agg
|
||||
]
|
||||
groupby_margin_indices = [
|
||||
i for i, field in enumerate(groupby)
|
||||
if 'margin' in field
|
||||
]
|
||||
if margin_indices or groupby_margin_indices:
|
||||
new_res = []
|
||||
offset = len(groupby)
|
||||
for row in res:
|
||||
row_list = list(row)
|
||||
for idx in margin_indices:
|
||||
if offset + idx < len(row_list):
|
||||
row_list[offset + idx] = 0.0
|
||||
for idx in groupby_margin_indices:
|
||||
if idx < len(row_list):
|
||||
row_list[idx] = 0.0
|
||||
new_res.append(tuple(row_list))
|
||||
return new_res
|
||||
return res
|
||||
|
||||
|
||||
def _hide_margin_fields_in_read_grouping_sets(env, grouping_sets, aggregates, res):
|
||||
if env.user.x_hide_margin:
|
||||
margin_indices = [
|
||||
idx for idx, agg in enumerate(aggregates)
|
||||
if 'margin' in agg
|
||||
]
|
||||
|
||||
new_res = []
|
||||
for groupby, group_results in zip(grouping_sets, res):
|
||||
groupby_margin_indices = [
|
||||
idx for idx, field in enumerate(groupby)
|
||||
if 'margin' in field
|
||||
]
|
||||
|
||||
if margin_indices or groupby_margin_indices:
|
||||
new_group_results = []
|
||||
offset = len(groupby)
|
||||
for row in group_results:
|
||||
row_list = list(row)
|
||||
for idx in margin_indices:
|
||||
if offset + idx < len(row_list):
|
||||
row_list[offset + idx] = 0.0
|
||||
for idx in groupby_margin_indices:
|
||||
if idx < len(row_list):
|
||||
row_list[idx] = 0.0
|
||||
new_group_results.append(tuple(row_list))
|
||||
new_res.append(new_group_results)
|
||||
else:
|
||||
new_res.append(group_results)
|
||||
return new_res
|
||||
return res
|
||||
|
||||
|
||||
def _hide_margin_fields_in_read(env, res):
|
||||
if env.user.x_hide_margin:
|
||||
for record in res:
|
||||
for key in list(record.keys()):
|
||||
if 'margin' in key:
|
||||
record[key] = 0.0
|
||||
return res
|
||||
|
||||
|
||||
class PosOrder(models.Model):
|
||||
_inherit = 'pos.order'
|
||||
|
||||
def _compute_margin(self):
|
||||
super()._compute_margin()
|
||||
if self.env.user.x_hide_margin:
|
||||
for order in self:
|
||||
order.margin = 0.0
|
||||
order.margin_percent = 0.0
|
||||
|
||||
@api.model
|
||||
def get_views(self, views, options=None):
|
||||
res = super().get_views(views, options)
|
||||
return _hide_margin_fields_in_view(self.env, res)
|
||||
|
||||
def fields_get(self, allfields=None, attributes=None):
|
||||
res = super().fields_get(allfields, attributes)
|
||||
return _hide_margin_fields_in_fields_get(self.env, res)
|
||||
|
||||
@api.model
|
||||
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
|
||||
res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
|
||||
return _hide_margin_fields_in_read_group_dict(self.env, res)
|
||||
|
||||
@api.model
|
||||
def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None):
|
||||
res = super()._read_group(domain, groupby, aggregates, having, offset, limit, order)
|
||||
return _hide_margin_fields_in_read_group(self.env, groupby, aggregates, res)
|
||||
|
||||
@api.model
|
||||
def _read_grouping_sets(self, domain, grouping_sets, aggregates=(), order=None):
|
||||
res = super()._read_grouping_sets(domain, grouping_sets, aggregates, order)
|
||||
return _hide_margin_fields_in_read_grouping_sets(self.env, grouping_sets, aggregates, res)
|
||||
|
||||
def read(self, fields=None, load='_classic_read'):
|
||||
res = super().read(fields, load=load)
|
||||
return _hide_margin_fields_in_read(self.env, res)
|
||||
|
||||
|
||||
class PosOrderLine(models.Model):
|
||||
_inherit = 'pos.order.line'
|
||||
|
||||
def _compute_margin(self):
|
||||
super()._compute_margin()
|
||||
if self.env.user.x_hide_margin:
|
||||
for line in self:
|
||||
line.margin = 0.0
|
||||
line.margin_percent = 0.0
|
||||
|
||||
@api.model
|
||||
def get_views(self, views, options=None):
|
||||
res = super().get_views(views, options)
|
||||
return _hide_margin_fields_in_view(self.env, res)
|
||||
|
||||
def fields_get(self, allfields=None, attributes=None):
|
||||
res = super().fields_get(allfields, attributes)
|
||||
return _hide_margin_fields_in_fields_get(self.env, res)
|
||||
|
||||
def read(self, fields=None, load='_classic_read'):
|
||||
res = super().read(fields, load=load)
|
||||
return _hide_margin_fields_in_read(self.env, res)
|
||||
|
||||
|
||||
class ReportPosOrder(models.Model):
|
||||
_inherit = 'report.pos.order'
|
||||
|
||||
@api.model
|
||||
def get_views(self, views, options=None):
|
||||
res = super().get_views(views, options)
|
||||
return _hide_margin_fields_in_view(self.env, res)
|
||||
|
||||
def fields_get(self, allfields=None, attributes=None):
|
||||
res = super().fields_get(allfields, attributes)
|
||||
return _hide_margin_fields_in_fields_get(self.env, res)
|
||||
|
||||
@api.model
|
||||
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
|
||||
res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
|
||||
return _hide_margin_fields_in_read_group_dict(self.env, res)
|
||||
|
||||
@api.model
|
||||
def _read_group(self, domain, groupby=(), aggregates=(), having=(), offset=0, limit=None, order=None):
|
||||
res = super()._read_group(domain, groupby, aggregates, having, offset, limit, order)
|
||||
return _hide_margin_fields_in_read_group(self.env, groupby, aggregates, res)
|
||||
|
||||
@api.model
|
||||
def _read_grouping_sets(self, domain, grouping_sets, aggregates=(), order=None):
|
||||
res = super()._read_grouping_sets(domain, grouping_sets, aggregates, order)
|
||||
return _hide_margin_fields_in_read_grouping_sets(self.env, grouping_sets, aggregates, res)
|
||||
|
||||
def read(self, fields=None, load='_classic_read'):
|
||||
res = super().read(fields, load=load)
|
||||
return _hide_margin_fields_in_read(self.env, res)
|
||||
13
models/res_users.py
Normal file
13
models/res_users.py
Normal file
@ -0,0 +1,13 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
x_hide_margin = fields.Boolean(string="Hide Margin", default=False)
|
||||
|
||||
@api.model
|
||||
def _load_pos_data_fields(self, config):
|
||||
fields_list = super()._load_pos_data_fields(config)
|
||||
fields_list.append('x_hide_margin')
|
||||
return fields_list
|
||||
12
readme.md
Normal file
12
readme.md
Normal file
@ -0,0 +1,12 @@
|
||||
# POS Hide Margin
|
||||
|
||||
This module adds a configuration option "Hide Margin" to the user preferences.
|
||||
When enabled, all margin and cost fields are hidden from both the POS backend views (including Pivot reports) and the POS Frontend app.
|
||||
|
||||
## Features
|
||||
|
||||
* Adds "Hide Margin" preference under the user form view.
|
||||
* Automatically zero-out margin calculations in the backend for users with this option enabled.
|
||||
* Hides the margin and cost details in the POS Frontend product info popup.
|
||||
* Hides margin and margin percent columns/fields from backend tree, form, and search views dynamically.
|
||||
* Zeros out margin field metrics in Pivot and Graph reports for affected users.
|
||||
@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ProductInfoPopup } from "@point_of_sale/app/components/popups/product_info_popup/product_info_popup";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ProductInfoPopup.prototype, {
|
||||
_hasMarginsCostsAccessRights() {
|
||||
const user = this.pos.user || this.pos.cashier || this.pos.getCashier?.();
|
||||
if (user && user.x_hide_margin) {
|
||||
return false;
|
||||
}
|
||||
return super._hasMarginsCostsAccessRights();
|
||||
}
|
||||
});
|
||||
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import test_pos_hide_margin
|
||||
206
tests/test_pos_hide_margin.py
Normal file
206
tests/test_pos_hide_margin.py
Normal file
@ -0,0 +1,206 @@
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestPosHideMargin(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Find the first active company
|
||||
active_company = cls.env['res.company'].search([], limit=1)
|
||||
if not active_company:
|
||||
# Fallback to any company if no active one exists
|
||||
active_company = cls.env['res.company'].with_context(active_test=False).search([], limit=1)
|
||||
|
||||
# Use the active company environment for all record creation in setup
|
||||
cls.env = cls.env['res.users'].with_company(active_company).env
|
||||
|
||||
# Create a user with x_hide_margin=True
|
||||
cls.restricted_user = cls.env['res.users'].create({
|
||||
'name': 'Restricted POS User',
|
||||
'login': 'restricted_pos_user',
|
||||
'email': 'restricted@example.com',
|
||||
'x_hide_margin': True,
|
||||
'group_ids': [(6, 0, [
|
||||
cls.env.ref('point_of_sale.group_pos_user').id,
|
||||
cls.env.ref('base.group_user').id,
|
||||
])],
|
||||
'company_id': active_company.id,
|
||||
'company_ids': [(6, 0, [active_company.id])],
|
||||
})
|
||||
|
||||
# Create a normal user with x_hide_margin=False
|
||||
cls.normal_user = cls.env['res.users'].create({
|
||||
'name': 'Normal POS User',
|
||||
'login': 'normal_pos_user',
|
||||
'email': 'normal@example.com',
|
||||
'x_hide_margin': False,
|
||||
'group_ids': [(6, 0, [
|
||||
cls.env.ref('point_of_sale.group_pos_user').id,
|
||||
cls.env.ref('base.group_user').id,
|
||||
])],
|
||||
'company_id': active_company.id,
|
||||
'company_ids': [(6, 0, [active_company.id])],
|
||||
})
|
||||
|
||||
# Create a product and template with cost and list price
|
||||
cls.product = cls.env['product.product'].create({
|
||||
'name': 'Margin Test Product',
|
||||
'list_price': 100.0,
|
||||
'standard_price': 60.0, # cost = 60
|
||||
})
|
||||
|
||||
# Create a POS Config & Session
|
||||
cls.pos_config = cls.env['pos.config'].create({
|
||||
'name': 'Test POS shop',
|
||||
'company_id': active_company.id,
|
||||
})
|
||||
cls.pos_session = cls.env['pos.session'].create({
|
||||
'config_id': cls.pos_config.id,
|
||||
'user_id': cls.env.uid,
|
||||
})
|
||||
|
||||
# Create a POS Order
|
||||
cls.pos_order = cls.env['pos.order'].create({
|
||||
'session_id': cls.pos_session.id,
|
||||
'partner_id': cls.env.ref('base.partner_admin').id,
|
||||
'company_id': active_company.id,
|
||||
'amount_total': 100.0,
|
||||
'amount_tax': 0.0,
|
||||
'amount_paid': 100.0,
|
||||
'amount_return': 0.0,
|
||||
'lines': [(0, 0, {
|
||||
'product_id': cls.product.id,
|
||||
'qty': 1,
|
||||
'price_unit': 100.0,
|
||||
'price_subtotal': 100.0,
|
||||
'price_subtotal_incl': 100.0,
|
||||
'total_cost': 60.0,
|
||||
'is_total_cost_computed': True,
|
||||
})]
|
||||
})
|
||||
|
||||
# Compute margins
|
||||
cls.pos_order._compute_margin()
|
||||
cls.pos_order.lines._compute_margin()
|
||||
|
||||
def test_margin_computation_normal_user(self):
|
||||
"""Test that margin is correctly calculated for a normal user."""
|
||||
order = self.pos_order.with_user(self.normal_user)
|
||||
order._compute_margin()
|
||||
# Cost is 60, price is 100 -> margin = 40, margin_percent = 40% (0.4)
|
||||
self.assertAlmostEqual(order.margin, 40.0)
|
||||
self.assertAlmostEqual(order.margin_percent, 0.4)
|
||||
|
||||
line = self.pos_order.lines[0].with_user(self.normal_user)
|
||||
line._compute_margin()
|
||||
self.assertAlmostEqual(line.margin, 40.0)
|
||||
|
||||
def test_margin_computation_restricted_user(self):
|
||||
"""Test that margin calculations are zeroed out for restricted users."""
|
||||
order = self.pos_order.with_user(self.restricted_user)
|
||||
order._compute_margin()
|
||||
self.assertEqual(order.margin, 0.0)
|
||||
self.assertEqual(order.margin_percent, 0.0)
|
||||
|
||||
line = self.pos_order.lines[0].with_user(self.restricted_user)
|
||||
line._compute_margin()
|
||||
self.assertEqual(line.margin, 0.0)
|
||||
self.assertEqual(line.margin_percent, 0.0)
|
||||
|
||||
def test_fields_get_restrictions(self):
|
||||
"""Test fields_get hides margin fields metadata for restricted users."""
|
||||
# Check for normal user
|
||||
fields_normal = self.env['pos.order'].with_user(self.normal_user).fields_get(['margin', 'margin_percent'])
|
||||
self.assertFalse(fields_normal.get('margin', {}).get('invisible', False))
|
||||
|
||||
# Check for restricted user
|
||||
fields_restricted = self.env['pos.order'].with_user(self.restricted_user).fields_get(['margin', 'margin_percent'])
|
||||
self.assertTrue(fields_restricted.get('margin', {}).get('invisible', False))
|
||||
self.assertFalse(fields_restricted.get('margin', {}).get('searchable', True))
|
||||
|
||||
def test_report_pos_order_read_restrictions(self):
|
||||
"""Test reading report.pos.order zeros out margins for restricted users."""
|
||||
# Create a report record via SQL reload
|
||||
self.env['report.pos.order'].init()
|
||||
report_records = self.env['report.pos.order'].search([('order_id', '=', self.pos_order.id)])
|
||||
self.assertTrue(report_records)
|
||||
|
||||
# Normal user reads report
|
||||
report_normal = report_records.with_user(self.normal_user).read(['margin'])
|
||||
self.assertAlmostEqual(report_normal[0]['margin'], 40.0)
|
||||
|
||||
# Restricted user reads report
|
||||
report_restricted = report_records.with_user(self.restricted_user).read(['margin'])
|
||||
self.assertEqual(report_restricted[0]['margin'], 0.0)
|
||||
|
||||
# Normal user read_group
|
||||
group_normal = self.env['report.pos.order'].with_user(self.normal_user).read_group(
|
||||
[('order_id', '=', self.pos_order.id)],
|
||||
['margin'],
|
||||
['product_categ_id']
|
||||
)
|
||||
self.assertAlmostEqual(group_normal[0]['margin'], 40.0)
|
||||
|
||||
# Restricted user read_group
|
||||
group_restricted = self.env['report.pos.order'].with_user(self.restricted_user).read_group(
|
||||
[('order_id', '=', self.pos_order.id)],
|
||||
['margin'],
|
||||
['product_categ_id']
|
||||
)
|
||||
self.assertEqual(group_restricted[0]['margin'], 0.0)
|
||||
|
||||
# Normal user _read_group
|
||||
_group_normal = self.env['report.pos.order'].with_user(self.normal_user)._read_group(
|
||||
[('order_id', '=', self.pos_order.id)],
|
||||
['product_categ_id'],
|
||||
['margin:sum']
|
||||
)
|
||||
self.assertAlmostEqual(_group_normal[0][1], 40.0)
|
||||
|
||||
# Restricted user _read_group
|
||||
_group_restricted = self.env['report.pos.order'].with_user(self.restricted_user)._read_group(
|
||||
[('order_id', '=', self.pos_order.id)],
|
||||
['product_categ_id'],
|
||||
['margin:sum']
|
||||
)
|
||||
self.assertEqual(_group_restricted[0][1], 0.0)
|
||||
|
||||
# Normal user _read_grouping_sets
|
||||
sets_normal = self.env['report.pos.order'].with_user(self.normal_user)._read_grouping_sets(
|
||||
[('order_id', '=', self.pos_order.id)],
|
||||
[['product_categ_id']],
|
||||
['margin:sum']
|
||||
)
|
||||
self.assertAlmostEqual(sets_normal[0][0][1], 40.0)
|
||||
|
||||
# Restricted user _read_grouping_sets
|
||||
sets_restricted = self.env['report.pos.order'].with_user(self.restricted_user)._read_grouping_sets(
|
||||
[('order_id', '=', self.pos_order.id)],
|
||||
[['product_categ_id']],
|
||||
['margin:sum']
|
||||
)
|
||||
self.assertEqual(sets_restricted[0][0][1], 0.0)
|
||||
|
||||
def test_pos_order_read_restrictions(self):
|
||||
"""Test reading pos.order and pos.order.line zeros out margins for restricted users."""
|
||||
# Normal user reads order
|
||||
order_normal = self.pos_order.with_user(self.normal_user).read(['margin', 'margin_percent'])
|
||||
self.assertAlmostEqual(order_normal[0]['margin'], 40.0)
|
||||
self.assertAlmostEqual(order_normal[0]['margin_percent'], 0.4)
|
||||
|
||||
# Restricted user reads order
|
||||
order_restricted = self.pos_order.with_user(self.restricted_user).read(['margin', 'margin_percent'])
|
||||
self.assertEqual(order_restricted[0]['margin'], 0.0)
|
||||
self.assertEqual(order_restricted[0]['margin_percent'], 0.0)
|
||||
|
||||
# Normal user reads line
|
||||
line_normal = self.pos_order.lines[0].with_user(self.normal_user).read(['margin', 'margin_percent'])
|
||||
self.assertAlmostEqual(line_normal[0]['margin'], 40.0)
|
||||
|
||||
# Restricted user reads line
|
||||
line_restricted = self.pos_order.lines[0].with_user(self.restricted_user).read(['margin', 'margin_percent'])
|
||||
self.assertEqual(line_restricted[0]['margin'], 0.0)
|
||||
self.assertEqual(line_restricted[0]['margin_percent'], 0.0)
|
||||
24
views/res_users_views.xml
Normal file
24
views/res_users_views.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_users_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.users.form.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='other_preferences']" position="inside">
|
||||
<field name="x_hide_margin"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_users_form_simple_modif_inherit" model="ir.ui.view">
|
||||
<field name="name">res.users.form.simple.modif.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='other_preferences']" position="inside">
|
||||
<field name="x_hide_margin"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user