Skip to content

Commit f49d93d

Browse files
committed
[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

0 commit comments

Comments
 (0)