[FIX] website: improve/fix menu, copy on unlink, clone page, unique_path, sale_report, ...
Fix bug from the initial poc of JOV
Fix menu creation
- creating a menu would create a 'container' menu in the DB (.create is call without website_id)
then writing on it would copy (if condition to cow) the menu with a website_id leaving the first
one as a menu container unused.
- website_menu should always have a website_id
we dont want to support the multiwebsite system that allow to have generic/specific menu
unlink/write is useless now since we always have a website_id
Make unique_path website dependent, 2 distinct website can have a page with same name
Make Sale report multiwebsite compliant
Make website_id on sale_order / account.invoice as a related stored from partner_id.
Co-authored-by: Derie Romain <rde@odoo.com>
Co-authored-by: Kersten Jérémy <jke@odoo.com>
This commit is contained in:
co-authored by
Derie Romain
parent
a45bb90fac
commit
602807acdf
@@ -11,8 +11,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
|
||||
auth_signup_reset_password = fields.Boolean(string='Enable password reset from Login page', config_parameter='auth_signup.reset_password')
|
||||
auth_signup_uninvited = fields.Selection([
|
||||
('b2b', 'On invitation (B2B)'),
|
||||
('b2c', 'Free sign up (B2C)'),
|
||||
('b2b', 'On invitation'),
|
||||
('b2c', 'Free sign up'),
|
||||
], string='Customer Account', default='b2b', config_parameter='auth_signup.invitation_scope')
|
||||
auth_signup_template_user_id = fields.Many2one('res.users', string='Template user for new users created through signup',
|
||||
config_parameter='base.template_portal_user_id')
|
||||
|
||||
@@ -52,8 +52,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_website_sale_digital = fields.Boolean("Sell digital products - provide downloadable content on your customer portal")
|
||||
|
||||
auth_signup_uninvited = fields.Selection([
|
||||
('b2b', 'On invitation (B2B)'),
|
||||
('b2c', 'Free sign up (B2C)'),
|
||||
('b2b', 'On invitation'),
|
||||
('b2c', 'Free sign up'),
|
||||
], string='Customer Account', default='b2b', config_parameter='auth_signup.invitation_scope')
|
||||
|
||||
module_delivery = fields.Boolean("Shipping Costs")
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
</record>
|
||||
|
||||
<record id="main_menu" model="website.menu">
|
||||
<field name="name">Top Menu</field>
|
||||
<field name="name">Top Menu</field>
|
||||
<field name="website_id" ref="default_website"/>
|
||||
</record>
|
||||
|
||||
<record id="menu_homepage" model="website.menu">
|
||||
@@ -18,6 +19,7 @@
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
<field name="website_id" ref="default_website"/>
|
||||
</record>
|
||||
|
||||
<record id="menu_contactus" model="website.menu">
|
||||
@@ -25,6 +27,7 @@
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
<field name="website_id" ref="default_website"/>
|
||||
</record>
|
||||
|
||||
<!-- split between ir_ui_view & website_page record to keep external_id on ir_ui_view -->
|
||||
|
||||
@@ -176,7 +176,7 @@ response = request.render("website.template_partner_comment", {
|
||||
<record id="website2_menu_contactus" model="website.menu">
|
||||
<field name="name">Contact us</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="parent_id" search="[('website_id', '=', ref('website2')), ('parent_id', '=', False)]"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
<field name="website_id" ref="website2"/>
|
||||
<field name="page_id" ref="website2_contactus_page" />
|
||||
|
||||
@@ -13,6 +13,13 @@ class Attachment(models.Model):
|
||||
# related for backward compatibility with saas-6
|
||||
website_url = fields.Char(string="Website URL", related='local_url', deprecated=True)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
website = self.env['website'].get_current_website()
|
||||
if website and 'website_id' not in vals and 'not_force_website_id' not in self.env.context:
|
||||
vals['website_id'] = website.id
|
||||
return super(Attachment, self).create(vals)
|
||||
|
||||
@api.model
|
||||
def get_serving_groups(self):
|
||||
return super(Attachment, self).get_serving_groups() + ['website.group_website_designer']
|
||||
|
||||
@@ -179,7 +179,7 @@ class Http(models.AbstractModel):
|
||||
return response
|
||||
except Exception as e:
|
||||
if 'werkzeug' in config['dev_mode'] and (not isinstance(exception, QWebException) or not exception.qweb.get('cause')):
|
||||
raise
|
||||
raise e
|
||||
exception = e
|
||||
|
||||
values = dict(
|
||||
|
||||
@@ -75,9 +75,8 @@ class View(models.Model):
|
||||
# reuse the COW mechanism to create
|
||||
# website-specific copies, it will take
|
||||
# care of creating pages and menus.
|
||||
view.with_context(website_id=website.id).write({'key': '%s [website %s]' % (view.key, website.id)})
|
||||
view.with_context(website_id=website.id).write({'name': view.name})
|
||||
|
||||
self |= self.with_context(active_test=False).search([('key', 'in', self.filtered('key').mapped('key'))])
|
||||
result = super(View, self).unlink()
|
||||
self.clear_caches()
|
||||
return result
|
||||
@@ -108,9 +107,6 @@ class View(models.Model):
|
||||
new_page = page.copy({
|
||||
'view_id': new_view.id,
|
||||
})
|
||||
for menu in page.menu_ids:
|
||||
# trigger COW
|
||||
menu.write({'page_id': new_page.id})
|
||||
|
||||
@api.model
|
||||
def get_related_views(self, key, bundles=False):
|
||||
|
||||
@@ -99,15 +99,15 @@ class Website(models.Model):
|
||||
installed_theme_id = fields.Many2one('ir.module.module')
|
||||
theme_ids = fields.Many2many('ir.module.module', 'website_theme', 'website_id', 'ir_module_module_id')
|
||||
auth_signup_uninvited = fields.Selection([
|
||||
('b2b', 'On invitation (B2B)'),
|
||||
('b2c', 'Free sign up (B2C)'),
|
||||
('b2b', 'On invitation'),
|
||||
('b2c', 'Free sign up'),
|
||||
], string='Customer Account', default='b2b')
|
||||
|
||||
@api.multi
|
||||
def _compute_menu(self):
|
||||
Menu = self.env['website.menu']
|
||||
for website in self:
|
||||
website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', 'in', (website.id, False))], order='id', limit=1).id
|
||||
website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', '=', website.id)], order='id', limit=1).id
|
||||
|
||||
# cf. Wizard hack in website_views.xml
|
||||
def noop(self, *args, **kwargs):
|
||||
@@ -162,6 +162,18 @@ class Website(models.Model):
|
||||
|
||||
self.homepage_id = self.env['website.page'].search([('website_id', '=', self.id),
|
||||
('key', '=', standard_homepage.key)])
|
||||
top_menu = self.env['website.menu'].create({
|
||||
'name': 'Top Menu for website ' + str(self.id),
|
||||
'website_id': self.id,
|
||||
'sequence': 0
|
||||
})
|
||||
self.menu_id = top_menu.id
|
||||
self.env['website.menu'].create({
|
||||
'name': 'Home', 'url': '/',
|
||||
'website_id': self.id,
|
||||
'parent_id': top_menu.id,
|
||||
'sequence': 10
|
||||
})
|
||||
|
||||
@api.model
|
||||
def new_page(self, name=False, add_menu=False, template='website.default_page', ispage=True, namespace=None):
|
||||
@@ -196,10 +208,11 @@ class Website(models.Model):
|
||||
if view.arch_fs:
|
||||
view.arch_fs = False
|
||||
|
||||
website = self.get_current_website()
|
||||
if ispage:
|
||||
page = self.env['website.page'].create({
|
||||
'url': page_url,
|
||||
'website_id': self.get_current_website().id,
|
||||
'website_id': website.id, # remove it if only one webiste or not?
|
||||
'view_id': view.id,
|
||||
})
|
||||
result['view_id'] = view.id
|
||||
@@ -207,9 +220,9 @@ class Website(models.Model):
|
||||
self.env['website.menu'].create({
|
||||
'name': name,
|
||||
'url': page_url,
|
||||
'parent_id': self.get_current_website().menu_id.id,
|
||||
'parent_id': website.menu_id.id,
|
||||
'page_id': page.id,
|
||||
'website_id': self.get_current_website().id,
|
||||
'website_id': website.id,
|
||||
})
|
||||
return result
|
||||
|
||||
@@ -222,7 +235,10 @@ class Website(models.Model):
|
||||
:param page_url : the url to be checked for uniqueness
|
||||
"""
|
||||
inc = 0
|
||||
domain_static = self.get_current_website().website_domain()
|
||||
# we only want a unique_path for website specific.
|
||||
# we need to be able to have /url for website=False, and /url for website=1
|
||||
# in case of duplicate, page manager will allow you to manage this case
|
||||
domain_static = [('website_id', '=', self.get_current_website().id)] # .website_domain()
|
||||
page_temp = page_url
|
||||
while self.env['website.page'].with_context(active_test=False).sudo().search([('url', '=', page_temp)] + domain_static):
|
||||
inc += 1
|
||||
@@ -877,7 +893,10 @@ class Page(models.Model):
|
||||
# is copied from.
|
||||
# (eg: website_version: an ir.ui.view record with the same key is
|
||||
# expected to be the same ir.ui.view but from another version)
|
||||
new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)'))})
|
||||
website_id = default.get('website_id')
|
||||
# website_id should be set during copy to avoid copying a generic page to another generic copy and then write on it
|
||||
# which would result in the generic page copied to a generic one and a specific one
|
||||
new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)')), 'website_id': website_id.id})
|
||||
default = {
|
||||
'name': '%s %s' % (self.name, _('(copy)')),
|
||||
'url': self.env['website'].get_unique_path(self.url),
|
||||
@@ -891,8 +910,10 @@ class Page(models.Model):
|
||||
:param page_id : website.page identifier
|
||||
"""
|
||||
page = self.browse(int(page_id))
|
||||
new_page = page.copy()
|
||||
if clone_menu:
|
||||
new_page = page.copy(dict(website_id=self.env['website'].get_current_website()))
|
||||
# Should not clone menu if the page was cloned from one website to another
|
||||
# Eg: Cloning a generic page (no website) will create a page with a website, we can't clone menu (not same container)
|
||||
if clone_menu and new_page.website_id == page.website_id:
|
||||
menu = self.env['website.menu'].search([('page_id', '=', page_id)], limit=1)
|
||||
if menu:
|
||||
# If the page being cloned has a menu, clone it too
|
||||
@@ -909,7 +930,7 @@ class Page(models.Model):
|
||||
for page in self:
|
||||
# Other pages linked to the ir_ui_view of the page being deleted (will it even be possible?)
|
||||
pages_linked_to_iruiview = self.search(
|
||||
[('view_id', '=', self.view_id.id), ('id', '!=', self.id)]
|
||||
[('view_id', '=', page.view_id.id), ('id', '!=', page.id)]
|
||||
)
|
||||
if len(pages_linked_to_iruiview) == 0 and not page.view_id.inherit_children_ids:
|
||||
# If there is no other pages linked to that ir_ui_view, we can delete the ir_ui_view
|
||||
@@ -946,37 +967,27 @@ class Menu(models.Model):
|
||||
parent_path = fields.Char(index=True)
|
||||
is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible')
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
'''This implements COU (copy-on-unlink). When deleting a generic menu
|
||||
item website-specific menu items will be created.'''
|
||||
current_website_id = self._context.get('website_id')
|
||||
|
||||
if current_website_id and not self._context.get('no_cow'):
|
||||
for menu in self.filtered(lambda menu: not menu.website_id):
|
||||
for website in self.env['website'].search([('id', '!=', current_website_id)]):
|
||||
# reuse the COW mechanism to create website-specific copies
|
||||
menu.with_context(website_id=website.id).write({})
|
||||
|
||||
return super(Menu, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
'''This implements COW (copy-on-write). This way editing websites does
|
||||
not impact other websites and newly created websites will only
|
||||
contain the default menus.
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
''' In case a menu without a website_id is trying to be created, we duplicate
|
||||
it for every website.
|
||||
Note: Particulary useful when installing a module that adds a menu like
|
||||
/shop. So every website has the shop menu.
|
||||
'''
|
||||
current_website_id = self._context.get('website_id')
|
||||
|
||||
if current_website_id and not self.website_id and not self._context.get('no_cow'):
|
||||
new_website_specific_menu = self.copy({'website_id': current_website_id})
|
||||
|
||||
for child in self.child_id:
|
||||
child.write({'parent_id': new_website_specific_menu.id})
|
||||
|
||||
return new_website_specific_menu.write(vals)
|
||||
|
||||
return super(Menu, self).write(vals)
|
||||
if vals.get('website_id'):
|
||||
return super(Menu, self).create(vals)
|
||||
elif self._context.get('website_id'):
|
||||
vals.update({'website_id': self._context.get('website_id')})
|
||||
return super(Menu, self).create(vals)
|
||||
else:
|
||||
# create for every site
|
||||
for website in self.env['website'].search([]):
|
||||
vals.update({
|
||||
'website_id': website.id,
|
||||
'parent_id': website.menu_id.id,
|
||||
})
|
||||
res = super(Menu, self).create(vals)
|
||||
return res # create loop, what to return ? last created record ?
|
||||
|
||||
@api.one
|
||||
def _compute_visible(self):
|
||||
@@ -1000,21 +1011,6 @@ class Menu(models.Model):
|
||||
url = '/%s' % self.url
|
||||
return url
|
||||
|
||||
def get_children_for_current_website(self):
|
||||
most_specific_child_menus = self.env['website.menu']
|
||||
website_id = self._context.get('website_id')
|
||||
|
||||
if not website_id:
|
||||
return self.child_id
|
||||
|
||||
for child in self.child_id:
|
||||
if child.website_id and child.website_id.id == website_id:
|
||||
most_specific_child_menus |= child
|
||||
elif not child.website_id and not any(child.clean_url() == child2.clean_url() and child2.website_id.id == website_id for child2 in self.child_id):
|
||||
most_specific_child_menus |= child
|
||||
|
||||
return most_specific_child_menus
|
||||
|
||||
# would be better to take a menu_id as argument
|
||||
@api.model
|
||||
def get_tree(self, website_id, menu_id=None):
|
||||
@@ -1031,7 +1027,7 @@ class Menu(models.Model):
|
||||
children=[],
|
||||
is_homepage=is_homepage,
|
||||
)
|
||||
for child in node.get_children_for_current_website():
|
||||
for child in node.child_id:
|
||||
menu_node['children'].append(make_tree(child))
|
||||
return menu_node
|
||||
if menu_id:
|
||||
@@ -1055,7 +1051,7 @@ class Menu(models.Model):
|
||||
mid = menu['id']
|
||||
# new menu are prefixed by new-
|
||||
if isinstance(mid, pycompat.string_types):
|
||||
new_menu = self.create({'name': menu['name']})
|
||||
new_menu = self.create({'name': menu['name'], 'website_id': website_id})
|
||||
replace_id(mid, new_menu.id)
|
||||
for menu in data['data']:
|
||||
menu_id = self.browse(menu['id'])
|
||||
|
||||
@@ -67,7 +67,7 @@ sAnimation.registry.affixMenu = sAnimation.Class.extend({
|
||||
|
||||
// Reset opened menus
|
||||
this.$dropdowns.removeClass('show');
|
||||
this.$navbarCollapses.removeClass('in').attr('aria-expanded', false);
|
||||
this.$navbarCollapses.removeClass('show').attr('aria-expanded', false);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ var WebsiteRoot = BodyManager.extend({
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
_multiWebsiteSwitch: function (ev) {
|
||||
var website_id_to_switch_to = ev.target.getAttribute('website-id');
|
||||
var website_id_to_switch_to = ev.currentTarget.getAttribute('website-id');
|
||||
this._rpc({
|
||||
route: '/website/force_website',
|
||||
params: {
|
||||
|
||||
@@ -542,7 +542,7 @@ var EditMenuDialog = weWidgets.Dialog.extend({
|
||||
this._rpc({
|
||||
model: 'website.menu',
|
||||
method: 'save',
|
||||
args: [[context.website_id], { data: data, to_delete: self.to_delete }],
|
||||
args: [context.website_id, { data: data, to_delete: self.to_delete }],
|
||||
context: context,
|
||||
}).then(function () {
|
||||
return _super();
|
||||
|
||||
@@ -55,7 +55,6 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
*/
|
||||
start: function () {
|
||||
var self = this;
|
||||
|
||||
this._generateDialogHTML();
|
||||
this.$modal.addClass('o_theme_customize_modal');
|
||||
|
||||
@@ -77,7 +76,7 @@ var ThemeCustomizeDialog = Dialog.extend({
|
||||
this._loadViews()
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -38,7 +38,7 @@ body.o_connected_user {
|
||||
}
|
||||
}
|
||||
.o_menu_systray {
|
||||
a {
|
||||
> li > a {
|
||||
padding: 0 $grid-gutter-width/2;
|
||||
|
||||
&.css_edit_dynamic{
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
<button class="btn btn-secondary js_date_range" data-date="month">Last Month</button>
|
||||
<button class="btn btn-secondary js_date_range" data-date="year">Last Year</button>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm pull-right" style="margin-right: 20px;">
|
||||
<div class="btn-group pull-right" style="margin-right: 20px;">
|
||||
<t t-foreach="widget.websites" t-as="website">
|
||||
<button t-attf-class="btn btn-sm btn-default js_website #{website.selected ? 'active' : ''}"
|
||||
<button t-attf-class="btn btn-default js_website #{website.selected ? 'active' : ''}"
|
||||
t-att-data-website-id="website.id">
|
||||
<t t-esc="website.name"/>
|
||||
</button>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<li class="nav-item"><a aria-controls="advances_page_info" role="tab" data-toggle="tab" class="nav-link" href="#advances_page_info">Publish</a></li>
|
||||
</ul>
|
||||
<div class="tab-content mt16">
|
||||
<div role="tabpanel" id="basic_page_info" class="tab-pane fade in active">
|
||||
<div role="tabpanel" id="basic_page_info" class="tab-pane fade show active">
|
||||
<div class="form-group row">
|
||||
<label class="col-form-label col-md-3" for="page_name">Page Name</label>
|
||||
<div class="col-md-9">
|
||||
@@ -88,7 +88,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" id="advances_page_info" class="tab-pane fade">
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<label class="control-label col-md-5" for="is_menu">Show in Top Menu</label>
|
||||
<div class="col-sm-2">
|
||||
<label class="o_switch" for="is_menu" >
|
||||
@@ -97,7 +97,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<label class="control-label col-md-5" for="is_homepage">Use as Homepage</label>
|
||||
<div class="col-sm-2">
|
||||
<label class="o_switch" for="is_homepage" >
|
||||
@@ -106,7 +106,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<label class="control-label col-md-5" for="is_indexed">
|
||||
Indexed
|
||||
<i class="fa fa-question-circle-o" title="Hide this page from search results" role="img" aria-label="Info"></i>
|
||||
@@ -118,7 +118,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<label class="control-label col-md-5" for="is_published">Publish</label>
|
||||
<div class="col-sm-2">
|
||||
<label class="o_switch js_publish_btn" for="is_published">
|
||||
@@ -127,7 +127,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if='widget.page.website_id' class="form-group" id="website_restriction">
|
||||
<div t-if='widget.page.website_id' class="form-group row" id="website_restriction">
|
||||
<!-- TODO hide with multi-website group -->
|
||||
<label class="control-label col-md-5" for="share_page_info">
|
||||
Share this page
|
||||
@@ -142,9 +142,9 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group row">
|
||||
<label class="control-label col-md-5" for="date_publish">Publishing Date</label>
|
||||
<div class="col-sm-7">
|
||||
<div class="col-md-7">
|
||||
<div class='input-group date' id='date_publish_container'>
|
||||
<input type='text' class="form-control" name="date_publish" id="date_publish" />
|
||||
<div class="input-group-append">
|
||||
|
||||
@@ -3,10 +3,19 @@ from odoo.tests import common
|
||||
|
||||
|
||||
class TestMenu(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestMenu, self).setUp()
|
||||
|
||||
def test_website_own_menu_container(self):
|
||||
Website = self.env['website']
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
total_menus = Menu.search_count([])
|
||||
Website.create({'name': 'new website'})
|
||||
self.assertEqual(total_menus + 2, Menu.search_count([]), "New website's bootstraping should have created a container menu and home menu")
|
||||
|
||||
def test_menu_got_duplicated(self):
|
||||
Menu = self.env['website.menu']
|
||||
total_menu_items = Menu.search_count([])
|
||||
|
||||
self.menu_root = Menu.create({
|
||||
'name': 'Root',
|
||||
})
|
||||
@@ -16,95 +25,27 @@ class TestMenu(common.TransactionCase):
|
||||
'parent_id': self.menu_root.id,
|
||||
})
|
||||
|
||||
self.menu_leaf1 = Menu.create({
|
||||
'name': 'Leaf 1',
|
||||
'parent_id': self.menu_child.id,
|
||||
})
|
||||
self.assertEqual(total_menu_items + 4, Menu.search_count([]), "Creating a menu without a website_id should create this menu for every website_id")
|
||||
|
||||
self.menu_leaf2 = Menu.create({
|
||||
'name': 'Leaf 2',
|
||||
'parent_id': self.menu_root.id,
|
||||
})
|
||||
|
||||
# create an extra website so we have 3 in total
|
||||
self.website_test = self.env['website'].create({'domain': 'test'})
|
||||
|
||||
def test_cow_leaf(self):
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
# backend write, no COW
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_leaf1.write({'name': 'Leaf 1 modified'})
|
||||
self.assertEqual(total_menu_items, Menu.search_count([]))
|
||||
|
||||
# write through website 1
|
||||
self.menu_leaf1.with_context(website_id=1).write({'name': 'Leaf 1 Website 1'})
|
||||
self.assertEqual(self.menu_leaf1.name, 'Leaf 1 modified')
|
||||
self.assertEqual(total_menu_items + 1, Menu.search_count([]))
|
||||
new_menu = Menu.search([('name', '=', 'Leaf 1 Website 1')])
|
||||
self.assertEqual(new_menu.website_id.id, 1)
|
||||
|
||||
def test_cow_root(self):
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
# backend write, no COW
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_root.write({'name': 'Root 1 modified'})
|
||||
self.assertEqual(total_menu_items, Menu.search_count([]))
|
||||
|
||||
# write through website 1
|
||||
self.menu_root.with_context(website_id=1).write({'name': 'Root Website 1'})
|
||||
self.assertEqual(self.menu_root.name, 'Root 1 modified')
|
||||
self.assertEqual(total_menu_items + 4, Menu.search_count([]))
|
||||
|
||||
# verify parent/child structure
|
||||
website_specific_leaf2 = Menu.search([('name', '=', 'Leaf 2'), ('website_id', '=', 1)])
|
||||
website_specific_leaf1 = Menu.search([('name', '=', 'Leaf 1'), ('website_id', '=', 1)])
|
||||
website_specific_child = Menu.search([('name', '=', 'Child'), ('website_id', '=', 1)])
|
||||
website_specific_root = Menu.search([('name', '=', 'Root Website 1'), ('website_id', '=', 1)])
|
||||
self.assertEqual(website_specific_leaf2.parent_id.id, website_specific_root.id)
|
||||
self.assertEqual(website_specific_leaf1.parent_id.id, website_specific_child.id)
|
||||
self.assertEqual(website_specific_child.parent_id.id, website_specific_root.id)
|
||||
|
||||
def test_cou_leaf(self):
|
||||
Menu = self.env['website.menu']
|
||||
|
||||
# backend unlink, no COU
|
||||
self.menu_leaf1.unlink()
|
||||
self.assertEqual(len(Menu.search([('name', '=', 'Leaf 1')])), 0)
|
||||
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_leaf2.with_context(website_id=1).unlink()
|
||||
|
||||
website_specific_menus = Menu.search([('name', '=', 'Leaf 2')])
|
||||
self.assertEqual(len(website_specific_menus), 2)
|
||||
self.assertEqual(set(website_specific_menus.mapped('website_id').ids), set([2, self.website_test.id]))
|
||||
|
||||
# exactly 2 website-specific menu items were created and one was deleted
|
||||
self.assertEqual(total_menu_items + 1, Menu.search_count([]))
|
||||
|
||||
def test_cou_root_backend(self):
|
||||
def test_menu_count(self):
|
||||
Menu = self.env['website.menu']
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_root.unlink()
|
||||
|
||||
# should have deleted root and all children
|
||||
self.assertEqual(total_menu_items - 4, Menu.search_count([]))
|
||||
top_menu = self.env['website'].get_current_website().menu_id
|
||||
data = [
|
||||
{
|
||||
'id': 'new-1',
|
||||
'parent_id': top_menu.id,
|
||||
'name': 'New Menu Specific 1',
|
||||
'url': '/new-specific-1',
|
||||
},
|
||||
{
|
||||
'id': 'new-2',
|
||||
'parent_id': top_menu.id,
|
||||
'name': 'New Menu Specific 2',
|
||||
'url': '/new-specific-2',
|
||||
}
|
||||
]
|
||||
Menu.save(1, {'data': data, 'to_delete': []})
|
||||
|
||||
def test_cou_root_frontend(self):
|
||||
Menu = self.env['website.menu']
|
||||
total_menu_items = Menu.search_count([])
|
||||
self.menu_root.with_context(website_id=1).unlink()
|
||||
|
||||
# should have removed 4 generic menus and created root and all children for 2 remaining websites
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Root'), ('website_id', '=', 2)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Child'), ('website_id', '=', 2)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 1'), ('website_id', '=', 2)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 2'), ('website_id', '=', 2)]), 1)
|
||||
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Root'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Child'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 1'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Leaf 2'), ('website_id', '=', self.website_test.id)]), 1)
|
||||
|
||||
self.assertEqual(total_menu_items + 4, Menu.search_count([]))
|
||||
self.assertEqual(total_menu_items + 2, Menu.search_count([]), "Creating 2 new menus should create only 2 menus records")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# coding: utf-8
|
||||
from odoo.tests import common
|
||||
from odoo.tests import common, HttpCase
|
||||
|
||||
|
||||
class TestPage(common.TransactionCase):
|
||||
@@ -31,8 +31,58 @@ class TestPage(common.TransactionCase):
|
||||
self.page_1_menu = Menu.create({
|
||||
'name': 'Page 1 menu',
|
||||
'page_id': self.page_1.id,
|
||||
'website_id': 1,
|
||||
})
|
||||
|
||||
def test_copy_page(self):
|
||||
View = self.env['ir.ui.view']
|
||||
Page = self.env['website.page']
|
||||
Menu = self.env['website.menu']
|
||||
# Specific page
|
||||
self.specific_view = View.create({
|
||||
'name': 'Base',
|
||||
'type': 'qweb',
|
||||
'arch': '<div>Specific View</div>',
|
||||
'key': 'test.specific_view',
|
||||
})
|
||||
self.page_specific = Page.create({
|
||||
'view_id': self.specific_view.id,
|
||||
'url': '/page_specific',
|
||||
'website_id': 1,
|
||||
})
|
||||
self.page_specific_menu = Menu.create({
|
||||
'name': 'Page Specific menu',
|
||||
'page_id': self.page_specific.id,
|
||||
'website_id': 1,
|
||||
})
|
||||
total_pages = Page.search_count([])
|
||||
total_menus = Menu.search_count([])
|
||||
# Copying a specific page should create a new page with an unique URL (suffixed by -X)
|
||||
Page.clone_page(self.page_specific.id, True)
|
||||
cloned_page = Page.search([('url', '=', '/page_specific-1')])
|
||||
cloned_menu = Menu.search([('url', '=', '/page_specific-1')])
|
||||
self.assertEqual(len(cloned_page), 1, "A page with an URL /page_specific-1 should've been created")
|
||||
self.assertEqual(Page.search_count([]), total_pages + 1, "Should have cloned the page")
|
||||
# It should also copy its menu with new url/name/page_id (if the page has a menu)
|
||||
self.assertEqual(len(cloned_menu), 1, "A specific page (with a menu) being cloned should have it's menu also cloned")
|
||||
self.assertEqual(cloned_menu.page_id, cloned_page, "The new cloned menu and the new cloned page should be linked (m2o)")
|
||||
self.assertEqual(Menu.search_count([]), total_menus + 1, "Should have cloned the page menu")
|
||||
|
||||
total_pages = Page.search_count([])
|
||||
total_menus = Menu.search_count([])
|
||||
|
||||
# Copying a generic page should create a specific page with same URL
|
||||
Page.clone_page(self.page_1.id, True)
|
||||
cloned_generic_page = Page.search([('url', '=', '/page_1'), ('id', '!=', self.page_1.id), ('website_id', '!=', False)])
|
||||
self.assertEqual(len(cloned_generic_page), 1, "A generic page being cloned should create a specific one for the current website")
|
||||
self.assertEqual(cloned_generic_page.url, self.page_1.url, "The URL of the cloned specific page should be the same as the generic page it has been cloned from")
|
||||
self.assertEqual(Page.search_count([]), total_pages + 1, "Should have cloned the generic page as a specific page for this website")
|
||||
self.assertEqual(Menu.search_count([]), total_menus, "It should not create a new menu as the generic page's menu belong to another website")
|
||||
# Except if the URL already exists for this website (its the case now that we already cloned it once)
|
||||
Page.clone_page(self.page_1.id, True)
|
||||
cloned_generic_page_2 = Page.search([('url', '=', '/page_1-1'), ('id', '!=', self.page_1.id)])
|
||||
self.assertEqual(len(cloned_generic_page_2), 1, "A generic page being cloned should create a specific page with a new URL if there is already a specific page with that URL")
|
||||
|
||||
def test_cow_page(self):
|
||||
Menu = self.env['website.menu']
|
||||
Page = self.env['website.page']
|
||||
@@ -50,27 +100,23 @@ class TestPage(common.TransactionCase):
|
||||
# edit through frontend
|
||||
self.page_1.with_context(website_id=1).write({'arch': '<div>website 1 content</div>'})
|
||||
|
||||
# should have created website-specific copies for:
|
||||
# - page
|
||||
# - menu
|
||||
# - view x2 (base view + extension view)
|
||||
# and shouldn't have touched original records
|
||||
# 1. should have created website-specific copies for:
|
||||
# - page
|
||||
# - view x2 (base view + extension view)
|
||||
# 2. should not have created menu copy as menus are not shared/COW
|
||||
# 3. and shouldn't have touched original records
|
||||
self.assertEqual(total_pages + 1, Page.search_count([]))
|
||||
self.assertEqual(total_menus + 1, Menu.search_count([]))
|
||||
self.assertEqual(total_menus, Menu.search_count([]))
|
||||
self.assertEqual(total_views + 2, View.search_count([]))
|
||||
|
||||
self.assertEqual(self.page_1.arch, '<div>modified base content</div>')
|
||||
self.assertEqual(bool(self.page_1.website_id), False)
|
||||
self.assertEqual(bool(self.page_1_menu.website_id), False)
|
||||
|
||||
new_page = Page.search([('url', '=', '/page_1'), ('id', '!=', self.page_1.id)])
|
||||
self.assertEqual(new_page.website_id.id, 1)
|
||||
self.assertEqual(new_page.view_id.inherit_children_ids[0].website_id.id, 1)
|
||||
self.assertEqual(new_page.arch, '<div>website 1 content</div>')
|
||||
|
||||
new_menu = new_page.menu_ids[0]
|
||||
self.assertEqual(new_menu.website_id.id, 1)
|
||||
|
||||
def test_cow_extension_view(self):
|
||||
''' test cow on extension view itself (like web_editor would do in the frontend) '''
|
||||
Menu = self.env['website.menu']
|
||||
@@ -98,14 +144,12 @@ class TestPage(common.TransactionCase):
|
||||
|
||||
self.assertEqual(self.extension_view.arch, '<div>modified extension content</div>')
|
||||
self.assertEqual(bool(self.page_1.website_id), False)
|
||||
self.assertEqual(bool(self.page_1_menu.website_id), False)
|
||||
|
||||
new_view = View.search([('name', '=', 'Extension'), ('website_id', '=', 1)])
|
||||
self.assertEqual(new_view.arch, '<div>website 1 content</div>')
|
||||
self.assertEqual(new_view.website_id.id, 1)
|
||||
|
||||
def test_cou_page_backend(self):
|
||||
Menu = self.env['website.menu']
|
||||
Page = self.env['website.page']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
@@ -113,12 +157,10 @@ class TestPage(common.TransactionCase):
|
||||
self.extension_view.unlink()
|
||||
|
||||
self.page_1.unlink()
|
||||
self.assertEqual(Menu.search_count([('name', '=', 'Page 1 menu')]), 0)
|
||||
self.assertEqual(Page.search_count([('url', '=', '/page_1')]), 0)
|
||||
self.assertEqual(View.search_count([('name', 'in', ('Base', 'Extension'))]), 0)
|
||||
|
||||
def test_cou_page_frontend(self):
|
||||
Menu = self.env['website.menu']
|
||||
Page = self.env['website.page']
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
@@ -129,8 +171,40 @@ class TestPage(common.TransactionCase):
|
||||
|
||||
self.assertEqual(bool(self.base_view.exists()), False)
|
||||
self.assertEqual(bool(self.page_1.exists()), False)
|
||||
# Not COU but deleting a page will delete its menu (cascade)
|
||||
self.assertEqual(bool(self.page_1_menu.exists()), False)
|
||||
|
||||
self.assertEqual(Menu.search([('name', '=', 'Page 1 menu')]).website_id.id, 2)
|
||||
self.assertEqual(Page.search([('url', '=', '/page_1')]).website_id.id, 2)
|
||||
self.assertEqual(View.search([('name', 'in', ('Base', 'Extension'))]).mapped('website_id').id, 2)
|
||||
|
||||
|
||||
class Crawler(HttpCase):
|
||||
def test_unpublished_page(self):
|
||||
Page = self.env['website.page']
|
||||
View = self.env['ir.ui.view']
|
||||
base_view = View.create({
|
||||
'name': 'Base',
|
||||
'type': 'qweb',
|
||||
'arch': '''<t name="Homepage" t-name="website.base_view">
|
||||
<t t-call="website.layout">
|
||||
I am a generic page
|
||||
</t>
|
||||
</t>''',
|
||||
'key': 'test.base_view',
|
||||
})
|
||||
generic_page = Page.create({
|
||||
'view_id': base_view.id,
|
||||
'url': '/page_1',
|
||||
'website_published': True,
|
||||
})
|
||||
|
||||
specific_page = generic_page.copy({'website_id': self.env['website'].get_current_website().id})
|
||||
specific_page.write({'website_published': False, 'arch': generic_page.arch.replace('I am a generic page', 'I am a specific page')})
|
||||
|
||||
r = self.url_open(specific_page.url)
|
||||
self.assertEqual(r.status_code, 404, "Restricted users should see a 404 and not the generic one as we unpublished the specific one")
|
||||
|
||||
self.authenticate('admin', 'admin')
|
||||
r = self.url_open(specific_page.url)
|
||||
self.assertEqual(r.status_code, 200, "Admin should see the specific unpublished page")
|
||||
self.assertEqual('I am a specific page' in r.text, True, "Admin should see the specific unpublished page")
|
||||
|
||||
@@ -330,7 +330,7 @@
|
||||
id="menu_website_websites_list"
|
||||
action="action_website_list"
|
||||
parent="menu_website_global_configuration"
|
||||
groups="website.group_multi_website"
|
||||
groups="base.group_no_one"
|
||||
sequence="10"
|
||||
/>
|
||||
|
||||
|
||||
@@ -77,20 +77,21 @@
|
||||
<li class="o_mobile_preview" id="mobile-menu">
|
||||
<a data-action="show-mobile-preview" href="#"><span title="Mobile preview" role="img" aria-label="Mobile preview" class="fa fa-mobile"/></a>
|
||||
</li>
|
||||
<li class="dropdown" groups="website.group_multi_website">
|
||||
<a class="dropdown-toggle waves" data-toggle="dropdown" href="#"><t t-esc="multi_website_selected_website['name']"/> <span class="caret"/></a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li t-as="multi_website_website" t-foreach="multi_website_websites">
|
||||
<a href="#">
|
||||
<t t-if="multi_website_selected_website['website_id'] != multi_website_website['website_id']">
|
||||
<span class="oe_menu_text js_multi_website_switch" t-att-website-id="str(multi_website_website['website_id'])">
|
||||
<t t-esc="multi_website_website['name']"/>
|
||||
</span>
|
||||
</t>
|
||||
<li groups="website.group_multi_website">
|
||||
<a class="dropdown-toggle waves" data-toggle="dropdown" href="#"><t t-esc="multi_website_selected_website['name']"/></a>
|
||||
<div class="dropdown-menu" role="menu">
|
||||
<t t-foreach="multi_website_websites" t-as="multi_website_website">
|
||||
<a role="menuitem" href="#"
|
||||
t-if="multi_website_selected_website['website_id'] != multi_website_website['website_id']"
|
||||
class="dropdown-item oe_menu_text js_multi_website_switch"
|
||||
t-att-website-id="str(multi_website_website['website_id'])"
|
||||
>
|
||||
<span t-esc="multi_website_website['name']" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Page Edition -->
|
||||
<li class="o_new_content_menu" id="new-content-menu">
|
||||
<a href="#"><span class="fa fa-plus"/>New</a>
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
<attribute name="t-if">website.user_id != user_id</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//header//ul[@id='top_menu']/li[hasclass('divider')]" position="before">
|
||||
<t t-foreach="website.menu_id.get_children_for_current_website()" t-as="submenu">
|
||||
<t t-foreach="website.menu_id.child_id" t-as="submenu">
|
||||
<t t-call="website.submenu">
|
||||
<t t-set="submenu_type" t-value="'nav'"/>
|
||||
</t>
|
||||
|
||||
@@ -159,8 +159,9 @@
|
||||
<tree string="Website Pages" default_order="name">
|
||||
<field name="name"/>
|
||||
<field name="url"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
<field name="website_indexed"/>
|
||||
<field name="website_published"/>
|
||||
<field name="website_published" string="Page Published"/>
|
||||
<field name="create_uid" invisible="1"/>
|
||||
<field name="write_uid"/>
|
||||
<field name="write_date"/>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<field name="url" eval="'/blog/'+str(ref('website_blog.blog_blog_1'))"/>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">40</field>
|
||||
<field name="website_id" ref="website.default_website"/>
|
||||
</record>
|
||||
|
||||
<!-- Pre loaded covers -->
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<field name="name" placeholder="Blog Post Title"/>
|
||||
<field name="subtitle" placeholder="Blog Subtitle"/>
|
||||
<field name="tag_ids" widget="many2many_tags"/>
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</group>
|
||||
<group string="Publishing Options">
|
||||
<field name="author_id"/>
|
||||
|
||||
@@ -84,7 +84,7 @@ class Event(models.Model):
|
||||
event.menu_id.unlink()
|
||||
elif event.website_menu:
|
||||
if not event.menu_id:
|
||||
root_menu = self.env['website.menu'].create({'name': event.name})
|
||||
root_menu = self.env['website.menu'].create({'name': event.name, 'website_id': event.id})
|
||||
event.menu_id = root_menu
|
||||
for sequence, (name, url, xml_id) in enumerate(event._get_menu_entries()):
|
||||
event._create_menu(sequence, name, url, xml_id)
|
||||
@@ -99,6 +99,7 @@ class Event(models.Model):
|
||||
'url': url,
|
||||
'parent_id': self.menu_id.id,
|
||||
'sequence': sequence,
|
||||
'website_id': self.id,
|
||||
})
|
||||
return menu
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<template id="s_newsletter_subscribe_popup" name="Newsletter Popup">
|
||||
<div class="o_newsletter_popup" data-list-id="0">
|
||||
<div class="alert alert-warning alert-dismissible fade in css_non_editable_mode_hidden o_not_editable" role="status">
|
||||
<div class="alert alert-warning alert-dismissible fade show css_non_editable_mode_hidden o_not_editable" role="status">
|
||||
<strong>Newsletter Popup!</strong> The newsletter popup snippet effect is active on this page. Click <a id="edit_dialog" href="#">Here To Edit Dialog Content</a>
|
||||
</div>
|
||||
<div role="dialog" class="modal modal-dialog modal-md fade fade-custom" id="o_newsletter_popup" tabindex="-1">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account_invoice
|
||||
from . import crm_team
|
||||
from . import ir_http
|
||||
from . import mail_compose_message
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountInvoice(models.Model):
|
||||
_inherit = 'account.invoice'
|
||||
|
||||
website_id = fields.Many2one('website', related='partner_id.website_id', string='Website',
|
||||
help='Website through which this invoice was created.',
|
||||
store=True, readonly=True)
|
||||
@@ -8,14 +8,7 @@ class SaleReport(models.Model):
|
||||
|
||||
website_id = fields.Many2one('website', readonly=True)
|
||||
|
||||
def _select(self):
|
||||
select = super(SaleReport, self)._select()
|
||||
select += ', s.website_id as website_id'
|
||||
|
||||
return select
|
||||
|
||||
def _group_by(self):
|
||||
group_by = super(SaleReport, self)._group_by()
|
||||
group_by += ', s.website_id'
|
||||
|
||||
return group_by
|
||||
def _query(self, with_clause='', fields={}, groupby='', from_clause=''):
|
||||
fields['website_id'] = ", s.website_id as website_id"
|
||||
groupby += ', s.website_id'
|
||||
return super(SaleReport, self)._query(with_clause, fields, groupby, from_clause)
|
||||
|
||||
@@ -21,4 +21,15 @@
|
||||
<field name="search_view_id" ref="product.product_pricelist_view_search" />
|
||||
<field name="context">{"default_base":'list_price'}</field>
|
||||
</record>
|
||||
|
||||
<record id="account_invoice_view_form" model="ir.ui.view">
|
||||
<field name="name">account.invoice.form</field>
|
||||
<field name="model">account.invoice</field>
|
||||
<field name="inherit_id" ref="account.invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="fiscal_position_id" position="after">
|
||||
<field name="website_id" groups="website.group_multi_website"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -224,7 +224,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<field name='currency_id' position='after'>
|
||||
<field name="selectable"/>
|
||||
<field name="website_id" string="Allow to use on " placeholder="None website" groups="website.group_multi_website"/>
|
||||
<field name="website_id" string="Allow to use on " placeholder="None website"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -171,9 +171,10 @@ class IrHttp(models.AbstractModel):
|
||||
if serve:
|
||||
return serve
|
||||
|
||||
# Don't handle exception but use werkeug debugger if server in --dev mode
|
||||
if 'werkzeug' in tools.config['dev_mode']:
|
||||
raise
|
||||
# Don't handle exception but use werkzeug debugger if server in --dev mode
|
||||
if 'werkzeug' in tools.config['dev_mode'] and not isinstance(exception, werkzeug.exceptions.NotFound):
|
||||
raise exception
|
||||
|
||||
try:
|
||||
return request._handle_exception(exception)
|
||||
except AccessDenied:
|
||||
|
||||
Reference in New Issue
Block a user