Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f49d93d

Browse files
committedMar 19, 2025·
[IMP] super_portal: Implement Super Portal User access
Features: - Created a 'Super Portal User' group with extended portal access. - Introduced 'Edit Portal Access' for managing email and portal access. - Restricted 'Portal View' modifications to Sales Administrators. - Modified contact form: renamed 'Other Address' to 'Company Address,' ensured correct contact type. - Implemented transaction filtering by contact in the portal (Sales Orders, Invoices, POs, Helpdesk). - Established a super branch and branch hierarchy for super and sub branches. - Recomputing prices based on billing address pricelist.
1 parent 460af3f commit f49d93d

14 files changed

+583
-0
lines changed
 

‎super_portal/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import controllers

‎super_portal/__manifest__.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
'name': 'Super Portal User',
3+
'category': 'Portal',
4+
'summary': 'Portal access for multi-branch management',
5+
'depends': ['contacts', 'website_sale'],
6+
'data': [
7+
'security/portal_security.xml',
8+
'views/res_partner_views.xml',
9+
'views/portal_wizard_views.xml',
10+
'views/templates.xml',
11+
],
12+
'assets': {
13+
'web.assets_frontend': [
14+
'super_portal/static/src/js/website_sale.js',
15+
'super_portal/static/src/js/address_search.js'
16+
],
17+
},
18+
'licence': 'LGPL-3',
19+
'installable': True,
20+
}

‎super_portal/controllers/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import main
2+
from . import portal

‎super_portal/controllers/main.py

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from odoo import http
2+
from odoo.http import request
3+
from odoo.addons.website_sale.controllers.main import WebsiteSale
4+
from werkzeug.exceptions import Forbidden
5+
6+
class WebsiteSalePortal(WebsiteSale):
7+
"""
8+
Extends WebsiteSale to modify checkout values and update the pricelist when billing address changes.
9+
"""
10+
11+
def checkout_values(self, order, **kw):
12+
order = order or request.website.sale_get_order(force_create=True)
13+
bill_partners = []
14+
ship_partners = []
15+
16+
if not order._is_public_order():
17+
Partner = order.partner_id.with_context(show_address=1).sudo()
18+
commercial_partner = order.partner_id.commercial_partner_id
19+
bill_partners = Partner.search([
20+
'|', ("type", "in", ["invoice", "other"]), ("id", "=", commercial_partner.id),
21+
("id", "child_of", commercial_partner.ids)
22+
], order='id asc, parent_id asc') | order.partner_id
23+
ship_partners = Partner.search([
24+
'|', ("type", "in", ["delivery", "other"]), ("id", "=", commercial_partner.id),
25+
("id", "child_of", commercial_partner.ids)
26+
], order='id asc, parent_id asc') | order.partner_id
27+
28+
if commercial_partner != order.partner_id:
29+
if not self._check_billing_partner_mandatory_fields(commercial_partner):
30+
bill_partners = bill_partners.filtered(lambda p: p.id != commercial_partner.id)
31+
if not self._check_shipping_partner_mandatory_fields(commercial_partner):
32+
ship_partners = ship_partners.filtered(lambda p: p.id != commercial_partner.id)
33+
34+
return {
35+
'order': order,
36+
'website_sale_order': order,
37+
'shippings': ship_partners,
38+
'billings': bill_partners,
39+
'only_services': order and order.only_services or False
40+
}
41+
42+
@http.route('/shop/cart/update_address', type='http', auth='public', methods=['POST'], website=True, csrf=False)
43+
def update_cart_address(self, partner_id, mode='billing', **kw):
44+
response = super().update_cart_address(partner_id, mode, **kw)
45+
46+
order_sudo = request.website.sale_get_order()
47+
if not order_sudo:
48+
return response
49+
50+
partner_sudo = request.env['res.partner'].sudo().browse(int(partner_id)).exists()
51+
if not partner_sudo:
52+
raise Forbidden()
53+
54+
new_pricelist = partner_sudo.property_product_pricelist
55+
if new_pricelist and new_pricelist != order_sudo.pricelist_id:
56+
order_sudo.write({'pricelist_id': new_pricelist.id})
57+
58+
order_sudo._recompute_prices()
59+
order_sudo._compute_amounts()
60+
order_sudo.sudo().write({
61+
'amount_total': order_sudo.amount_total,
62+
'amount_tax': order_sudo.amount_tax,
63+
'amount_untaxed': order_sudo.amount_untaxed
64+
})
65+
66+
return response
67+
68+
@http.route(['/shop/cart/update_total'], type='json', auth='public', methods=['POST'], website=True, csrf=False)
69+
def cart_update_total(self):
70+
order_sudo = request.website.sale_get_order()
71+
if not order_sudo:
72+
return {"error": "No active order found"}
73+
74+
line_items = [
75+
{
76+
"unit_price": line.price_unit,
77+
"subtotal": line.price_subtotal,
78+
}
79+
for line in order_sudo.order_line
80+
]
81+
82+
return {
83+
"amount_untaxed": order_sudo.amount_untaxed,
84+
"amount_tax": order_sudo.amount_tax,
85+
"amount_total": order_sudo.amount_total,
86+
"cart_quantity": order_sudo.cart_quantity,
87+
"line_items": line_items,
88+
}
89+
90+
@http.route(['/shop/confirm_order'], type='http', auth="public", website=True, sitemap=False)
91+
def confirm_order(self, **post):
92+
order = request.website.sale_get_order()
93+
94+
redirection = self.checkout_redirection(order) or self.checkout_check_address(order)
95+
if redirection:
96+
return redirection
97+
98+
order.order_line._compute_tax_id()
99+
# request.website.sale_get_order(update_pricelist=True)
100+
extra_step = request.website.viewref('website_sale.extra_info')
101+
if extra_step.active:
102+
return request.redirect("/shop/extra_info")
103+
104+
return request.redirect("/shop/payment")

‎super_portal/controllers/portal.py

+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
from collections import OrderedDict
2+
from markupsafe import Markup
3+
from operator import itemgetter
4+
5+
from odoo import _, http
6+
from odoo.http import request
7+
from odoo.addons.portal.controllers.portal import CustomerPortal, pager as portal_pager
8+
from odoo.osv.expression import OR, AND
9+
from odoo.tools import groupby as groupbyelem
10+
11+
12+
class CustomPortal(CustomerPortal):
13+
# filter based on super and sub branch
14+
def _get_searchbar_filters(self):
15+
partner = request.env.user.partner_id
16+
filters = OrderedDict({'all': {'label': _('All'), 'domain': []}})
17+
18+
partner_ids = request.env['res.partner'].search([
19+
('id', 'child_of', partner.id),
20+
('is_company', '=', True)
21+
])
22+
23+
for p in partner_ids:
24+
filters[str(p.id)] = {
25+
'label': p.name,
26+
'domain': [('partner_id', '=', p.id)],
27+
}
28+
return filters
29+
30+
# invoices & bills
31+
def _get_account_searchbar_filters(self):
32+
branch_filters = self._get_searchbar_filters()
33+
filters = OrderedDict({
34+
'all': {'label': _('All'), 'domain': []},
35+
'invoices': {'label': _('Invoices'), 'domain': [('move_type', 'in', ('out_invoice', 'out_refund', 'out_receipt'))]},
36+
'bills': {'label': _('Bills'), 'domain': [('move_type', 'in', ('in_invoice', 'in_refund', 'in_receipt'))]},
37+
})
38+
filters.update(branch_filters)
39+
40+
return filters
41+
42+
# your orders
43+
def _get_sale_searchbar_filters(self):
44+
return self._get_searchbar_filters()
45+
46+
def _prepare_sale_portal_rendering_values(
47+
self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, quotation_page=False, **kwargs
48+
):
49+
SaleOrder = request.env['sale.order']
50+
51+
if not sortby:
52+
sortby = 'date'
53+
54+
partner = request.env.user.partner_id
55+
values = self._prepare_portal_layout_values()
56+
57+
if quotation_page:
58+
url = "/my/quotes"
59+
domain = self._prepare_quotations_domain(partner)
60+
else:
61+
url = "/my/orders"
62+
domain = self._prepare_orders_domain(partner)
63+
64+
searchbar_sortings = self._get_sale_searchbar_sortings()
65+
searchbar_filters = self._get_sale_searchbar_filters()
66+
if not filterby:
67+
filterby = 'all'
68+
domain += searchbar_filters[filterby]['domain']
69+
70+
sort_order = searchbar_sortings[sortby]['order']
71+
72+
if date_begin and date_end:
73+
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
74+
75+
pager_values = portal_pager(
76+
url=url,
77+
total=SaleOrder.search_count(domain),
78+
page=page,
79+
step=self._items_per_page,
80+
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'filterby': filterby},
81+
)
82+
orders = SaleOrder.search(domain, order=sort_order, limit=self._items_per_page, offset=pager_values['offset'])
83+
84+
values.update({
85+
'date': date_begin,
86+
'quotations': orders.sudo() if quotation_page else SaleOrder,
87+
'orders': orders.sudo() if not quotation_page else SaleOrder,
88+
'page_name': 'quote' if quotation_page else 'order',
89+
'pager': pager_values,
90+
'default_url': url,
91+
'searchbar_sortings': searchbar_sortings,
92+
'sortby': sortby,
93+
'searchbar_filters': searchbar_filters,
94+
'filterby': filterby
95+
})
96+
97+
return values
98+
99+
# our orders
100+
@http.route(['/my/purchase', '/my/purchase/page/<int:page>'], type='http', auth="user", website=True)
101+
def portal_my_purchase_orders(
102+
self, page=1, date_begin=None, date_end=None, sortby=None, filterby=None, **kw
103+
):
104+
return self._render_portal(
105+
"purchase.portal_my_purchase_orders",
106+
page, date_begin, date_end, sortby, filterby,
107+
[],
108+
self._get_searchbar_filters(),
109+
'all',
110+
"/my/purchase",
111+
'my_purchases_history',
112+
'purchase',
113+
'orders'
114+
)
115+
116+
def _prepare_my_tickets_values(
117+
self, page=1, date_begin=None, date_end=None, sortby=None, filterby='all', search=None, groupby='none', search_in='content'
118+
):
119+
values = self._prepare_portal_layout_values()
120+
domain = self._prepare_helpdesk_tickets_domain()
121+
122+
searchbar_sortings = {
123+
'date': {'label': _('Newest'), 'order': 'create_date desc'},
124+
'reference': {'label': _('Reference'), 'order': 'id desc'},
125+
'name': {'label': _('Subject'), 'order': 'name'},
126+
'user': {'label': _('Assigned to'), 'order': 'user_id'},
127+
'stage': {'label': _('Stage'), 'order': 'stage_id'},
128+
'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc'},
129+
}
130+
searchbar_filters = self._get_searchbar_filters()
131+
searchbar_inputs = {
132+
'content': {'input': 'content', 'label': Markup(_('Search <span class="nolabel"> (in Content)</span>'))},
133+
'ticket_ref': {'input': 'ticket_ref', 'label': _('Search in Reference')},
134+
'message': {'input': 'message', 'label': _('Search in Messages')},
135+
'user': {'input': 'user', 'label': _('Search in Assigned to')},
136+
'status': {'input': 'status', 'label': _('Search in Stage')},
137+
}
138+
searchbar_groupby = {
139+
'none': {'input': 'none', 'label': _('None')},
140+
'stage': {'input': 'stage_id', 'label': _('Stage')},
141+
'user': {'input': 'user_id', 'label': _('Assigned to')},
142+
}
143+
144+
# default sort by value
145+
if not sortby:
146+
sortby = 'date'
147+
order = searchbar_sortings[sortby]['order']
148+
if groupby in searchbar_groupby and groupby != 'none':
149+
order = f'{searchbar_groupby[groupby]["input"]}, {order}'
150+
151+
if filterby in ['last_message_sup', 'last_message_cust']:
152+
discussion_subtype_id = request.env.ref('mail.mt_comment').id
153+
messages = request.env['mail.message'].search_read([('model', '=', 'helpdesk.ticket'), ('subtype_id', '=', discussion_subtype_id)], fields=['res_id', 'author_id'], order='date desc')
154+
last_author_dict = {}
155+
for message in messages:
156+
if message['res_id'] not in last_author_dict:
157+
last_author_dict[message['res_id']] = message['author_id'][0]
158+
159+
ticket_author_list = request.env['helpdesk.ticket'].search_read(fields=['id', 'partner_id'])
160+
ticket_author_dict = dict([(ticket_author['id'], ticket_author['partner_id'][0] if ticket_author['partner_id'] else False) for ticket_author in ticket_author_list])
161+
162+
last_message_cust = []
163+
last_message_sup = []
164+
ticket_ids = set(last_author_dict.keys()) & set(ticket_author_dict.keys())
165+
for ticket_id in ticket_ids:
166+
if last_author_dict[ticket_id] == ticket_author_dict[ticket_id]:
167+
last_message_cust.append(ticket_id)
168+
else:
169+
last_message_sup.append(ticket_id)
170+
171+
if filterby == 'last_message_cust':
172+
domain = AND([domain, [('id', 'in', last_message_cust)]])
173+
else:
174+
domain = AND([domain, [('id', 'in', last_message_sup)]])
175+
176+
else:
177+
domain = AND([domain, searchbar_filters[filterby]['domain']])
178+
179+
if date_begin and date_end:
180+
domain = AND([domain, [('create_date', '>', date_begin), ('create_date', '<=', date_end)]])
181+
182+
# search
183+
if search and search_in:
184+
search_domain = []
185+
if search_in == 'ticket_ref':
186+
search_domain = OR([search_domain, [('ticket_ref', 'ilike', search)]])
187+
if search_in == 'content':
188+
search_domain = OR([search_domain, ['|', ('name', 'ilike', search), ('description', 'ilike', search)]])
189+
if search_in == 'user':
190+
assignees = request.env['res.users'].sudo()._search([('name', 'ilike', search)])
191+
search_domain = OR([search_domain, [('user_id', 'in', assignees)]])
192+
if search_in == 'message':
193+
discussion_subtype_id = request.env.ref('mail.mt_comment').id
194+
search_domain = OR([search_domain, [('message_ids.body', 'ilike', search), ('message_ids.subtype_id', '=', discussion_subtype_id)]])
195+
if search_in == 'status':
196+
search_domain = OR([search_domain, [('stage_id', 'ilike', search)]])
197+
domain = AND([domain, search_domain])
198+
199+
# pager
200+
tickets_count = request.env['helpdesk.ticket'].search_count(domain)
201+
pager = portal_pager(
202+
url="/my/tickets",
203+
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'search_in': search_in, 'search': search, 'groupby': groupby, 'filterby': filterby},
204+
total=tickets_count,
205+
page=page,
206+
step=self._items_per_page
207+
)
208+
209+
tickets = request.env['helpdesk.ticket'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
210+
request.session['my_tickets_history'] = tickets.ids[:100]
211+
212+
if not tickets:
213+
grouped_tickets = []
214+
elif groupby != 'none':
215+
grouped_tickets = [request.env['helpdesk.ticket'].concat(*g) for k, g in groupbyelem(tickets, itemgetter(searchbar_groupby[groupby]['input']))]
216+
else:
217+
grouped_tickets = [tickets]
218+
219+
values.update({
220+
'date': date_begin,
221+
'grouped_tickets': grouped_tickets,
222+
'page_name': 'ticket',
223+
'default_url': '/my/tickets',
224+
'pager': pager,
225+
'searchbar_sortings': searchbar_sortings,
226+
'searchbar_filters': searchbar_filters,
227+
'searchbar_inputs': searchbar_inputs,
228+
'searchbar_groupby': searchbar_groupby,
229+
'sortby': sortby,
230+
'groupby': groupby,
231+
'search_in': search_in,
232+
'search': search,
233+
'filterby': filterby,
234+
})
235+
return values

‎super_portal/models/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import res_partner
2+
from . import portal_wizard

‎super_portal/models/portal_wizard.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from odoo import api, fields, models
2+
3+
4+
class PortalWizardUser(models.TransientModel):
5+
_inherit = 'portal.wizard.user'
6+
7+
can_edit = fields.Boolean(compute='_compute_can_edit', string="Can Edit")
8+
9+
@api.depends('user_id', 'user_id.groups_id')
10+
def _compute_can_edit(self):
11+
"""Compute if the user can edit based on access rights"""
12+
13+
for portal_wizard_user in self:
14+
user = portal_wizard_user.user_id
15+
if user.has_group('super_portal.group_edit_portal_access'):
16+
portal_wizard_user.can_edit = True
17+
else:
18+
portal_wizard_user.can_edit = False

‎super_portal/models/res_partner.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from odoo import api, fields, models
2+
3+
4+
class ResPartner(models.Model):
5+
_inherit = 'res.partner'
6+
7+
type = fields.Selection([
8+
('contact', "Contact"),
9+
('invoice', "Invoice Address"),
10+
('delivery', "Delivery Address"),
11+
('other', "Company Address")
12+
], string="Address Type", default='contact')
13+
14+
res_partner_id = fields.Many2one(
15+
'res.partner',
16+
string="Related Company",
17+
domain="[('is_company', '=', True)]"
18+
)
19+
20+
@api.model
21+
def create(self, vals):
22+
if 'parent_id' in vals and vals['parent_id']:
23+
vals['is_company'] = True
24+
return super(ResPartner, self).create(vals)
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<odoo>
2+
<record id="group_edit_portal_access" model="res.groups">
3+
<field name="name">Edit Portal Access</field>
4+
<field name="category_id" ref="base.module_category_usability"/>
5+
</record>
6+
7+
<!-- Restrict Editing of Portal View -->
8+
<record id="edit_portal_access_rule" model="ir.rule">
9+
<field name="name">Restrict Portal Access Editing</field>
10+
<field name="model_id" ref="portal.model_portal_wizard"/>
11+
<field name="groups" eval="[(4, ref('super_portal.group_edit_portal_access'))]"/>
12+
<field name="perm_read" eval="1"/>
13+
<field name="perm_write" eval="1"/>
14+
<field name="perm_create" eval="0"/>
15+
<field name="perm_unlink" eval="0"/>
16+
</record>
17+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/** @odoo-module **/
2+
3+
import publicWidget from "@web/legacy/js/public/public_widget";
4+
5+
function filterAddresses(inputElement, sectionClass) {
6+
let searchTerm = inputElement.value.toLowerCase();
7+
let addressTiles = document.querySelectorAll(`.${sectionClass} .one_kanban`);
8+
9+
addressTiles.forEach((tile) => {
10+
let textContent = tile.innerText.toLowerCase();
11+
let isSelected = tile.querySelector('.card')?.classList.contains('bg-primary');
12+
13+
tile.style.display = textContent.includes(searchTerm) || isSelected ? 'block' : 'none';
14+
});
15+
}
16+
window.filterAddresses = filterAddresses;
17+
18+
publicWidget.registry.AddressSearch = publicWidget.Widget.extend({
19+
selector: '.o_billing_address_search, .o_shipping_address_search',
20+
21+
events: {
22+
'keyup .o_billing_address_search': '_onKeyUpBilling',
23+
'keyup .o_shipping_address_search': '_onKeyUpShipping',
24+
},
25+
26+
_onKeyUpBilling: function (ev) {
27+
filterAddresses(ev.currentTarget, 'all_billing');
28+
},
29+
30+
_onKeyUpShipping: function (ev) {
31+
filterAddresses(ev.currentTarget, 'all_shipping');
32+
},
33+
});
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/** @odoo-module **/
2+
3+
import publicWidget from "@web/legacy/js/public/public_widget";
4+
import { jsonrpc } from "@web/core/network/rpc_service";
5+
6+
publicWidget.registry.websiteSaleCart = publicWidget.registry.websiteSaleCart.extend({
7+
events: Object.assign({}, publicWidget.registry.websiteSaleCart.prototype.events, {
8+
'click .js_change_billing': '_onClickChangeBilling',
9+
}),
10+
11+
_onClickChangeBilling: function (ev) {
12+
var self = this;
13+
var rowAddrClass = "all_billing";
14+
var cardClass = "js_change_billing";
15+
16+
var $old = $(`.${rowAddrClass}`).find('.card.border.border-primary');
17+
$old.find('.btn-addr').toggle();
18+
$old.addClass(cardClass);
19+
$old.removeClass('bg-primary border border-primary');
20+
21+
var $new = $(ev.currentTarget).parent('div.one_kanban').find('.card');
22+
$new.find('.btn-addr').toggle();
23+
$new.removeClass(cardClass);
24+
$new.addClass('bg-primary border border-primary');
25+
26+
var $form = $(ev.currentTarget).parent('div.one_kanban').find('form.d-none');
27+
$.post($form.attr('action'), $form.serialize() + '&xhr=1').done(function () {
28+
self._updateCartTotals();
29+
});
30+
},
31+
32+
_updateCartTotals: function () {
33+
jsonrpc("/shop/cart/update_total", {})
34+
.then((data) => {
35+
if (!data.cart_quantity) {
36+
return window.location.reload();
37+
}
38+
$("#order_total_untaxed .oe_currency_value").html(data.amount_untaxed.toFixed(2));
39+
$("#order_total_taxes .oe_currency_value").html(data.amount_tax.toFixed(2));
40+
$("#order_total .oe_currency_value").html(data.amount_total.toFixed(2));
41+
$("#amount_total_summary .oe_currency_value").html(data.amount_total.toFixed(2));
42+
43+
let $cartRows = $("#cart_products tr");
44+
data.line_items.forEach((line, index) => {
45+
let $row = $cartRows.eq(index);
46+
$row.find(".oe_currency_value").text(line.subtotal.toFixed(2));
47+
});
48+
})
49+
.catch((err) => {
50+
console.error("Error updating cart totals:", err);
51+
});
52+
}
53+
});
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<odoo>
2+
<record id="wizard_view_inherit" model="ir.ui.view">
3+
<field name="name">portal.wizard.view.inherit</field>
4+
<field name="model">portal.wizard</field>
5+
<field name="inherit_id" ref="portal.wizard_view"/>
6+
<field name="arch" type="xml">
7+
<xpath expr="//field[@name='is_internal']" position="after">
8+
<field name="can_edit" column_invisible="True"/>
9+
</xpath>
10+
<!-- Restrict Email Editing -->
11+
<xpath expr="//field[@name='user_ids']/tree/field[@name='email']" position="attributes">
12+
<attribute name="readonly">not can_edit</attribute>
13+
</xpath>
14+
15+
<!-- Hide "Grant Access," "Revoke Access," and "Re-Invite" Buttons for Unauthorized Users -->
16+
<xpath expr="//button[@name='action_grant_access']" position="attributes">
17+
<attribute name="groups">super_portal.group_edit_portal_access</attribute>
18+
</xpath>
19+
<xpath expr="//button[@name='action_revoke_access']" position="attributes">
20+
<attribute name="groups">super_portal.group_edit_portal_access</attribute>
21+
</xpath>
22+
<xpath expr="//button[@name='action_invite_again']" position="attributes">
23+
<attribute name="groups">super_portal.group_edit_portal_access</attribute>
24+
</xpath>
25+
</field>
26+
</record>
27+
</odoo>
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<odoo>
2+
<record id="view_partner_form" model="ir.ui.view">
3+
<field name="name">res.partner.form.view</field>
4+
<field name="model">res.partner</field>
5+
<field name="inherit_id" ref="base.view_partner_form"/>
6+
<field name="arch" type="xml">
7+
<!-- Ensure Parent Company field is visible & editable -->
8+
<xpath expr="//field[@name='parent_id']" position="attributes">
9+
<attribute name="invisible">not is_company</attribute>
10+
<attribute name="readonly">not is_company</attribute>
11+
</xpath>
12+
13+
<!-- Ensure Address Type (type) is editable and Text(Address) invisible -->
14+
<xpath expr="//field[@name='type']" position="attributes">
15+
<attribute name="invisible">False</attribute>
16+
</xpath>
17+
<xpath expr="//b[contains(text(), 'Address')]" position="attributes">
18+
<attribute name="invisible">True</attribute>
19+
</xpath>
20+
</field>
21+
</record>
22+
</odoo>

‎super_portal/views/templates.xml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<template id="portal_checkout_override" inherit_id="website_sale.checkout">
4+
<xpath expr="//h4[contains(text(), 'Billing')]" position="after">
5+
<div class="row mb-2">
6+
<div class="col-lg-12">
7+
<input type="text" class="form-control o_billing_address_search"
8+
placeholder="Search Billing Address..."
9+
onkeyup="filterAddresses(this, 'all_billing')"/>
10+
</div>
11+
</div>
12+
</xpath>
13+
14+
<xpath expr="//h4[contains(text(), 'Shipping')]" position="after">
15+
<div class="row mb-2">
16+
<div class="col-lg-12">
17+
<input type="text" class="form-control o_shipping_address_search"
18+
placeholder="Search Shipping Address..."
19+
onkeyup="filterAddresses(this, 'all_shipping')"/>
20+
</div>
21+
</div>
22+
</xpath>
23+
</template>
24+
</odoo>

0 commit comments

Comments
 (0)
Please sign in to comment.