[FIX] *: share URL on the correct website if possible
* = base, auth_signup, mail, portal, sale, website, website_sale Before this commit, sending an object by email would always link to web.base.url even if the object was created from a specific website. Now if the object has a website, we use the URL of that website if it is set. The fallback will always be on the web.base.url. opw-1921030 PR: #30000
This commit is contained in:
@@ -53,8 +53,8 @@ class ResPartner(models.Model):
|
||||
the url state components (menu_id, id, view_type) """
|
||||
|
||||
res = dict.fromkeys(self.ids, False)
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for partner in self:
|
||||
base_url = partner.get_base_url()
|
||||
# when required, make sure the partner has a valid signup token
|
||||
if self.env.context.get('signup_valid') and not partner.user_ids:
|
||||
partner.sudo().signup_prepare()
|
||||
|
||||
@@ -231,7 +231,9 @@
|
||||
<td align="center" style="min-width: 590px;">
|
||||
<t t-set="access_action" t-value="record.with_context(force_website=True).get_access_action()"/>
|
||||
<t t-set="is_online" t-value="access_action and access_action['type'] == 'ir.actions.act_url'"/>
|
||||
<t t-set="access_url" t-value="is_online and record._get_share_url(redirect=True, signup_partner=True) or ''"/>
|
||||
<t t-set="base_url" t-value="record.get_base_url()"/>
|
||||
<t t-set="share_url" t-value="record._get_share_url(redirect=True, signup_partner=True)"/>
|
||||
<t t-set="access_url" t-value="is_online and share_url and base_url + share_url or ''"/>
|
||||
<t t-set="access_name">
|
||||
View <t t-esc="model_description or 'document'"/>
|
||||
</t>
|
||||
|
||||
@@ -35,6 +35,16 @@ class PortalMixin(models.AbstractModel):
|
||||
self.sudo().write({'access_token': str(uuid.uuid4())})
|
||||
return self.access_token
|
||||
|
||||
@api.multi
|
||||
def get_base_url(self):
|
||||
"""Get the base URL for the current model.
|
||||
|
||||
Defined here to be overriden by website specific models.
|
||||
The method has to be public because it is called from mail templates.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
|
||||
def _get_share_url(self, redirect=False, signup_partner=False, pid=None):
|
||||
"""
|
||||
Build the url of the record that will be sent by mail and adds additional parameters such as
|
||||
|
||||
@@ -10,9 +10,10 @@ class PortalShare(models.TransientModel):
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(PortalShare, self).default_get(fields)
|
||||
record = self.env[result['res_model']].browse(result['res_id'])
|
||||
result['res_model'] = self._context.get('active_model')
|
||||
result['res_id'] = self._context.get('active_id')
|
||||
result['share_link'] = self.env[result['res_model']].browse(result['res_id'])._get_share_url(redirect=True)
|
||||
result['share_link'] = record.get_base_url() + record._get_share_url(redirect=True)
|
||||
return result
|
||||
|
||||
res_model = fields.Char('Related Document Model', required=True)
|
||||
@@ -24,12 +25,11 @@ class PortalShare(models.TransientModel):
|
||||
|
||||
@api.depends('res_model', 'res_id')
|
||||
def _compute_share_link(self):
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
for rec in self:
|
||||
res_model = self.env[rec.res_model]
|
||||
if isinstance(res_model, self.pool['portal.mixin']):
|
||||
record = res_model.browse(rec.res_id)
|
||||
rec.share_link = base_url + record._get_share_url(redirect=True)
|
||||
rec.share_link = record.get_base_url() + record._get_share_url(redirect=True)
|
||||
|
||||
@api.depends('res_model', 'res_id')
|
||||
def _compute_access_warning(self):
|
||||
@@ -52,7 +52,7 @@ class PortalShare(models.TransientModel):
|
||||
partner_ids = self.partner_ids.filtered(lambda x: x.user_ids)
|
||||
# if partner already user or record has access token send common link in batch to all user
|
||||
for partner in self.partner_ids:
|
||||
share_link = active_record._get_share_url(redirect=True, pid=partner.id)
|
||||
share_link = active_record.get_base_url() + active_record._get_share_url(redirect=True, pid=partner.id)
|
||||
active_record.with_context(mail_post_autofollow=True).message_post_with_view(template,
|
||||
values={'partner': partner, 'note': self.note, 'record': active_record,
|
||||
'share_link': share_link},
|
||||
@@ -67,7 +67,7 @@ class PortalShare(models.TransientModel):
|
||||
share_link = partner._get_signup_url_for_action(action='/mail/view', res_id=self.res_id, model=self.model)[partner.id]
|
||||
active_record.with_context(mail_post_autofollow=True).message_post_with_view(template,
|
||||
values={'partner': partner, 'note': self.note, 'record': active_record,
|
||||
'share_link': share_link },
|
||||
'share_link': share_link},
|
||||
subject=_("You are invited to access %s" % active_record.display_name),
|
||||
subtype_id=note.id,
|
||||
notif_layout='mail.mail_notification_light',
|
||||
|
||||
@@ -884,7 +884,14 @@ class SaleOrder(models.Model):
|
||||
self.ensure_one()
|
||||
return '%s %s' % (self.type_name, self.name)
|
||||
|
||||
@api.multi
|
||||
def _get_share_url(self, redirect=False, signup_partner=False, pid=None):
|
||||
"""Override for sales order.
|
||||
|
||||
If the SO is in a state where an action is required from the partner,
|
||||
return the URL with a login token. Otherwise, return the URL with a
|
||||
generic access token (no login).
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.state not in ['sale', 'done']:
|
||||
auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id])
|
||||
|
||||
@@ -53,3 +53,10 @@ class Partner(models.Model):
|
||||
# onchange uses the cache to retrieve value, we need to copy computed_value into the initial env
|
||||
for record, record2 in izip(self, self2):
|
||||
record.display_name = record2.display_name
|
||||
|
||||
@api.multi
|
||||
def get_base_url(self):
|
||||
"""When using multi-website, we want the user to be redirected to the
|
||||
most appropriate website if possible."""
|
||||
res = super(Partner, self).get_base_url()
|
||||
return self.website_id and self.website_id._get_http_domain() or res
|
||||
|
||||
@@ -731,6 +731,19 @@ class Website(models.Model):
|
||||
'target': 'self',
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def _get_http_domain(self):
|
||||
"""Get the domain of the current website, prefixed by http if no
|
||||
scheme is specified.
|
||||
|
||||
Empty string if no domain is specified on the website.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.domain:
|
||||
return ''
|
||||
res = urls.url_parse(self.domain)
|
||||
return 'http://' + self.domain if not res.scheme else self.domain
|
||||
|
||||
|
||||
class SeoMetadata(models.AbstractModel):
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
odoo.define('website.tour_reset_password', function (require) {
|
||||
'use strict';
|
||||
|
||||
var base = require('web_editor.base');
|
||||
var localStorage = require('web.local_storage');
|
||||
var rpc = require('web.rpc');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var currentDomain = window.location.protocol + '//' + window.location.hostname;
|
||||
var emailsUrl = '/web#action=mail.action_view_mail_mail&view_type=list';
|
||||
var usersUrl = '/web#action=base.action_res_users&view_type=list';
|
||||
var resetLinkKey = 'website.tour_reset_password.resetLink';
|
||||
|
||||
tour.register('website_reset_password', {
|
||||
test: true,
|
||||
url: '/web',
|
||||
wait_for: base.ready(),
|
||||
},
|
||||
[
|
||||
{
|
||||
content: "Change the domain of the websites and go to users page",
|
||||
trigger: '.oe_topbar_name:contains("Admin")',
|
||||
run: function () {
|
||||
// We could do it with the UI but this is not the goal of this test,
|
||||
// so we just make an RPC to be faster.
|
||||
|
||||
// We change the domain of the website to test that the email that
|
||||
// will be sent uses the correct domain for its links.
|
||||
var def1 = rpc.query({
|
||||
'model': 'website',
|
||||
'method': 'write',
|
||||
'args': [[1], {
|
||||
'domain': "my-test-domain.com",
|
||||
}],
|
||||
});
|
||||
// We need to change the domain of all the websites otherwise the
|
||||
// website selector will return the website 2 since the domain we
|
||||
// set on website 1 doesn't actually match our test server.
|
||||
var def2 = rpc.query({
|
||||
'model': 'website',
|
||||
'method': 'write',
|
||||
'args': [[2], {
|
||||
'domain': "https://domain-not-used.fr",
|
||||
}],
|
||||
});
|
||||
// Set a website on "Admin" partner to test the URL of the website.
|
||||
var def3 = rpc.query({
|
||||
model: 'res.partner',
|
||||
method: 'name_search',
|
||||
kwargs: {'name': 'Mitchell Admin'},
|
||||
}).then(function (res) {
|
||||
return rpc.query({
|
||||
'model': 'res.partner',
|
||||
'method': 'write',
|
||||
'args': [[res[0][0]], {
|
||||
'website_id': 1,
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
return $.when(def1, def2, def3).then(function () {
|
||||
window.location.href = usersUrl;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "click on Admin",
|
||||
trigger: '.o_data_cell:contains("Admin")',
|
||||
},
|
||||
{
|
||||
content: "click on reset password",
|
||||
trigger: '.btn[name="action_reset_password"]',
|
||||
},
|
||||
{
|
||||
content: "wait mail to be sent, and go see it",
|
||||
trigger: 'a[name="signup_url"][href^="http://my-test-domain.com"]',
|
||||
run: function () {
|
||||
window.location.href = emailsUrl;
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "click on the first email",
|
||||
trigger: '.o_data_cell:contains("Password reset"):eq(0)',
|
||||
},
|
||||
{
|
||||
content: "check page has an iframe",
|
||||
trigger: 'iframe body',
|
||||
run: function () {
|
||||
var content = $('iframe').contents()[0];
|
||||
console.log(content && new XMLSerializer().serializeToString(content));
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "check iframe has the button",
|
||||
trigger: 'iframe a:contains("Change password")',
|
||||
run: function () {},
|
||||
},
|
||||
{
|
||||
content: "check the URL is correct too",
|
||||
trigger: 'iframe a:contains("Change password")[href^="http://my-test-domain.com"]',
|
||||
run: function () {
|
||||
// reset the domain of the websites, go to users page
|
||||
return rpc.query({
|
||||
'model': 'website',
|
||||
'method': 'write',
|
||||
'args': [[1, 2], {
|
||||
'domain': "",
|
||||
}],
|
||||
}).then(function () {
|
||||
window.location.href = usersUrl;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "click on Admin",
|
||||
trigger: '.o_data_cell:contains("Admin")',
|
||||
},
|
||||
{
|
||||
content: "click on reset password",
|
||||
trigger: '.btn[name="action_reset_password"]',
|
||||
},
|
||||
{
|
||||
content: "wait mail to be sent, and go see it",
|
||||
trigger: 'a[name="signup_url"][href^="' + currentDomain + '"]',
|
||||
run: function () {
|
||||
window.location.href = emailsUrl;
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "click on the first email",
|
||||
trigger: '.o_data_cell:contains("Password reset"):eq(0)',
|
||||
},
|
||||
{
|
||||
content: "check the link has the current host, save the link, logout",
|
||||
trigger: 'iframe a:contains("Change password")[href^="' + currentDomain + '"]',
|
||||
run: function () {
|
||||
var link = $('iframe').contents().find('a:contains("Change password")').attr('href');
|
||||
localStorage.setItem(resetLinkKey, link);
|
||||
window.location.href = "/web/session/logout?redirect=/";
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "go to the reset link",
|
||||
trigger: 'a[href="/web/login"]',
|
||||
run: function () {
|
||||
window.location.href = localStorage.getItem(resetLinkKey);
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "fill new password and submit",
|
||||
trigger: '.oe_reset_password_form',
|
||||
run: function () {
|
||||
var $form = $('.oe_reset_password_form');
|
||||
$form.find('input[name="password"]').val('adminadmin');
|
||||
// password must be at least 8 if the module `auth_password_policy`
|
||||
// is installed which is the case on runbot
|
||||
$form.find('input[name="confirm_password"]').val('adminadmin');
|
||||
$form.find('button[type="submit"]').click();
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "check logged in, and reset admin website",
|
||||
trigger: '.oe_topbar_name:contains("Admin")',
|
||||
run: function () {
|
||||
return rpc.query({
|
||||
model: 'res.partner',
|
||||
method: 'name_search',
|
||||
kwargs: {'name': 'Admin'},
|
||||
}).then(function (res) {
|
||||
return rpc.query({
|
||||
'model': 'res.partner',
|
||||
'method': 'write',
|
||||
'args': [[res[0][0]], {
|
||||
'website_id': false,
|
||||
}],
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -8,3 +8,4 @@ from . import test_ui
|
||||
from . import test_views
|
||||
from . import test_menu
|
||||
from . import test_page
|
||||
from . import test_website_reset_password
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
try:
|
||||
from unittest.mock import patch
|
||||
except ImportError:
|
||||
from mock import patch
|
||||
|
||||
import odoo
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestWebsiteResetPassword(HttpCase):
|
||||
|
||||
def test_01_website_reset_password_tour(self):
|
||||
"""The goal of this test is to make sure the reset password works."""
|
||||
|
||||
# We override unlink because we don't want the email to be auto deleted
|
||||
# if the send works.
|
||||
MailMail = odoo.addons.mail.models.mail_mail.MailMail
|
||||
|
||||
# We override send_mail because in HttpCase on runbot we don't have an
|
||||
# SMTP server, so if force_send is set, the test is going to fail.
|
||||
MailTemplate = odoo.addons.mail.models.mail_template.MailTemplate
|
||||
original_send_mail = MailTemplate.send_mail
|
||||
|
||||
def my_send_mail(*args, **kwargs):
|
||||
kwargs.update(force_send=False)
|
||||
return original_send_mail(*args, **kwargs)
|
||||
|
||||
with patch.object(MailMail, 'unlink', lambda self: None), patch.object(MailTemplate, 'send_mail', my_send_mail):
|
||||
self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('website_reset_password')", "odoo.__DEBUG__.services['web_tour.tour'].tours.website_reset_password.ready", login="admin")
|
||||
@@ -14,6 +14,12 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_common" name="Website Assets Common" inherit_id="web.assets_common">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website/static/src/js/tours/website_tour_reset_password.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_backend" inherit_id="web.assets_backend" name="Website Backend Assets (used in backend interface)">
|
||||
<xpath expr="//link[last()]" position="after">
|
||||
<link rel="stylesheet" type="text/scss" href="/website/static/src/scss/website.backend.scss"/>
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<hr/>
|
||||
% endif
|
||||
<div style="text-align: center; margin: 16px 0px 16px 0px; font-size: 14px;">
|
||||
<a href="/shop/cart?access_token=${object.access_token}"
|
||||
<a href="${object.get_base_url()}/shop/cart?access_token=${object.access_token}"
|
||||
target="_blank"
|
||||
style="background-color: #875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||
Resume order
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class AccountInvoice(models.Model):
|
||||
@@ -10,3 +10,10 @@ class AccountInvoice(models.Model):
|
||||
website_id = fields.Many2one('website', related='partner_id.website_id', string='Website',
|
||||
help='Website through which this invoice was created.',
|
||||
store=True, readonly=True)
|
||||
|
||||
@api.multi
|
||||
def get_base_url(self):
|
||||
"""When using multi-website, we want the user to be redirected to the
|
||||
most appropriate website if possible."""
|
||||
res = super(AccountInvoice, self).get_base_url()
|
||||
return self.website_id and self.website_id._get_http_domain() or res
|
||||
|
||||
@@ -338,6 +338,13 @@ class SaleOrder(models.Model):
|
||||
},
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def get_base_url(self):
|
||||
"""When using multi-website, we want the user to be redirected to the
|
||||
most appropriate website if possible."""
|
||||
res = super(SaleOrder, self).get_base_url()
|
||||
return self.website_id and self.website_id._get_http_domain() or res
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
odoo.define('website_sale.tour_shop_mail', function (require) {
|
||||
'use strict';
|
||||
|
||||
var base = require('web_editor.base');
|
||||
var rpc = require('web.rpc');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('shop_mail', {
|
||||
test: true,
|
||||
url: '/',
|
||||
wait_for: base.ready(),
|
||||
},
|
||||
[
|
||||
{
|
||||
content: "Change the domain of the websites and go to shop",
|
||||
trigger: 'body',
|
||||
run: function () {
|
||||
// We change the domain of the website to test that the email that
|
||||
// will be sent uses the correct domain for its links.
|
||||
var def1 = rpc.query({
|
||||
'model': 'website',
|
||||
'method': 'write',
|
||||
'args': [[1], {
|
||||
'domain': "my-test-domain.com",
|
||||
}],
|
||||
});
|
||||
// We need to change the domain of all the websites otherwise the
|
||||
// website selector will return the website 2 since the domain we
|
||||
// set on website 1 doesn't actually match our test server.
|
||||
var def2 = rpc.query({
|
||||
'model': 'website',
|
||||
'method': 'write',
|
||||
'args': [[2], {
|
||||
'domain': "https://domain-not-used.fr",
|
||||
}],
|
||||
});
|
||||
$.when(def1, def2).then(function (data) {
|
||||
window.location.href = '/shop?search=Acoustic Bloc Screens';
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "select Acoustic Bloc Screens",
|
||||
trigger: '.oe_product_cart a:containsExact("Acoustic Bloc Screens")',
|
||||
},
|
||||
{
|
||||
content: "click add to cart",
|
||||
trigger: '#product_details #add_to_cart',
|
||||
},
|
||||
{
|
||||
content: "check product is in cart, get cart id, go to backend",
|
||||
trigger: 'td.td-product_name:contains("Acoustic Bloc Screens")',
|
||||
run: function () {
|
||||
rpc.query({
|
||||
'model': 'website',
|
||||
'method': 'sale_get_order',
|
||||
'args': [[$('html').data('website-id')]],
|
||||
}).then(function (data) {
|
||||
var orderId = parseInt(data.match(/sale\.order\((\d+),\)/)[1]);
|
||||
window.location.href = "/web#action=sale.action_orders&view_type=form&id=" + orderId;
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "click confirm",
|
||||
trigger: '.btn[name="action_confirm"]',
|
||||
},
|
||||
{
|
||||
content: "click send by email",
|
||||
trigger: '.btn[name="action_quotation_send"]',
|
||||
extra_trigger: '.o_statusbar_status .btn-primary:contains("Sales Order")',
|
||||
},
|
||||
{
|
||||
content: "click Send email",
|
||||
trigger: '.btn[name="action_send_mail"]',
|
||||
},
|
||||
{
|
||||
content: "wait mail to be sent, and go see it",
|
||||
trigger: '.o_thread_message_content:contains("Here is"):contains("the order")',
|
||||
run: function () {
|
||||
window.location.href = "/web#action=mail.action_view_mail_mail&view_type=list";
|
||||
},
|
||||
},
|
||||
{
|
||||
content: "click on the first email",
|
||||
trigger: '.o_data_cell:contains("YourCompany Order")',
|
||||
},
|
||||
{
|
||||
content: "check it's the correct email, and the URL is correct too",
|
||||
trigger: 'iframe p:contains("Here is"):contains("the order")',
|
||||
extra_trigger: 'iframe a[href^="http://my-test-domain.com"]',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
from . import test_customize
|
||||
from . import test_sale_process
|
||||
from . import test_website_sale_mail
|
||||
from . import test_website_sale_pricelist
|
||||
from . import test_website_sale_product_attribute_value_config
|
||||
from . import test_website_sale_zoom
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
try:
|
||||
from unittest.mock import patch
|
||||
except ImportError:
|
||||
from mock import patch
|
||||
|
||||
import odoo
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import HttpCase
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestWebsiteSaleMail(HttpCase):
|
||||
|
||||
def test_01_shop_mail_tour(self):
|
||||
"""The goal of this test is to make sure sending SO by email works."""
|
||||
|
||||
# we override unlink because we don't want the email to be auto deleted
|
||||
MailMail = odoo.addons.mail.models.mail_mail.MailMail
|
||||
|
||||
with patch.object(MailMail, 'unlink', lambda self: None):
|
||||
self.browser_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('shop_mail')", "odoo.__DEBUG__.services['web_tour.tour'].tours.shop_mail.ready", login="admin")
|
||||
@@ -46,6 +46,7 @@
|
||||
<template id="assets_common" name="tour" inherit_id="web.assets_common">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop.js"></script>
|
||||
<script type="text/javascript" src="/website_sale/static/src/js/website_sale_tour_shop_mail.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -868,6 +868,12 @@ class Partner(models.Model):
|
||||
def _get_country_name(self):
|
||||
return self.country_id.name or ''
|
||||
|
||||
@api.multi
|
||||
def get_base_url(self):
|
||||
"""Get the base URL for the current partner."""
|
||||
self.ensure_one()
|
||||
return self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
|
||||
|
||||
class ResPartnerIndustry(models.Model):
|
||||
_description = 'Industry'
|
||||
|
||||
Reference in New Issue
Block a user