summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
Diffstat (limited to 'addons')
-rw-r--r--addons/auth_signup/controllers/main.py2
-rw-r--r--addons/auth_signup/models/res_partner.py2
-rw-r--r--addons/auth_signup/models/res_users.py6
-rw-r--r--addons/website/__manifest__.py2
-rw-r--r--addons/website/controllers/backend.py9
-rw-r--r--addons/website/controllers/main.py10
-rw-r--r--addons/website/data/website_data.xml8
-rw-r--r--addons/website/data/website_demo.xml64
-rw-r--r--addons/website/models/__init__.py1
-rw-r--r--addons/website/models/ir_attachment.py49
-rw-r--r--addons/website/models/ir_http.py22
-rw-r--r--addons/website/models/ir_rule.py12
-rw-r--r--addons/website/models/ir_ui_view.py152
-rw-r--r--addons/website/models/res_company.py16
-rw-r--r--addons/website/models/res_config_settings.py25
-rw-r--r--addons/website/models/res_partner.py5
-rw-r--r--addons/website/models/res_users.py42
-rw-r--r--addons/website/models/website.py305
-rw-r--r--addons/website/static/src/js/backend/dashboard.js21
-rw-r--r--addons/website/static/src/js/content/website_root.js18
-rw-r--r--addons/website/static/src/js/menu/content.js13
-rw-r--r--addons/website/static/src/xml/website.backend.xml8
-rw-r--r--addons/website/static/src/xml/website.pageProperties.xml19
-rw-r--r--addons/website/tests/__init__.py2
-rw-r--r--addons/website/tests/test_menu.py110
-rw-r--r--addons/website/tests/test_page.py136
-rw-r--r--addons/website/tests/test_views.py83
-rw-r--r--addons/website/views/ir_attachment_views.xml14
-rw-r--r--addons/website/views/res_config_settings_views.xml111
-rw-r--r--addons/website/views/res_partner_views.xml13
-rw-r--r--addons/website/views/website_navbar_templates.xml14
-rw-r--r--addons/website/views/website_templates.xml20
-rw-r--r--addons/website/views/website_views.xml43
-rw-r--r--addons/website/wizard/base_language_install_views.xml2
-rw-r--r--addons/website_crm/views/res_config_settings_views.xml2
-rw-r--r--addons/website_livechat/views/res_config_settings_views.xml1
-rw-r--r--addons/website_sale/__init__.py1
-rw-r--r--addons/website_sale/controllers/backend.py9
-rw-r--r--addons/website_sale/controllers/main.py3
-rw-r--r--addons/website_sale/data/demo.xml3
-rw-r--r--addons/website_sale/models/product.py2
-rw-r--r--addons/website_sale/models/sale_order.py3
-rw-r--r--addons/website_sale/models/website.py1
-rw-r--r--addons/website_sale/report/__init__.py2
-rw-r--r--addons/website_sale/report/sale_report.py21
-rw-r--r--addons/website_sale/views/product_views.xml7
-rw-r--r--addons/website_sale/views/sale_order_views.xml10
-rw-r--r--addons/website_sale_link_tracker/controllers/backend.py4
-rw-r--r--addons/website_sale_stock/__manifest__.py3
-rw-r--r--addons/website_sale_stock/models/__init__.py1
-rw-r--r--addons/website_sale_stock/models/stock_picking.py14
-rw-r--r--addons/website_sale_stock/views/stock_picking_views.xml13
-rw-r--r--addons/website_theme_install/__manifest__.py1
-rw-r--r--addons/website_theme_install/models/__init__.py2
-rw-r--r--addons/website_theme_install/models/ir_module_module.py133
-rw-r--r--addons/website_theme_install/models/ir_ui_view.py35
-rw-r--r--addons/website_theme_install/models/res_config_settings.py14
-rw-r--r--addons/website_theme_install/views/res_config_settings_views.xml13
-rw-r--r--addons/website_theme_install/views/views.xml18
59 files changed, 1480 insertions, 195 deletions
diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py
index 6a3f055ecf7..cffc8f00fd5 100644
--- a/addons/auth_signup/controllers/main.py
+++ b/addons/auth_signup/controllers/main.py
@@ -95,7 +95,7 @@ class AuthSignupHome(Home):
get_param = request.env['ir.config_parameter'].sudo().get_param
return {
- 'signup_enabled': get_param('auth_signup.invitation_scope', 'b2b') == 'b2c',
+ 'signup_enabled': request.env['res.users']._get_signup_invitation_scope() == 'b2c',
'reset_password_enabled': get_param('auth_signup.reset_password') == 'True',
}
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index c0da1d85f83..d8dc57490dd 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -105,7 +105,7 @@ class ResPartner(models.Model):
"""
res = defaultdict(dict)
- allow_signup = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') == 'b2c'
+ allow_signup = self.env['res.users']._get_signup_invitation_scope() == 'b2c'
for partner in self:
if allow_signup and not partner.user_ids:
partner = partner.sudo()
diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py
index fdc2a66b1ee..a7a5e187092 100644
--- a/addons/auth_signup/models/res_users.py
+++ b/addons/auth_signup/models/res_users.py
@@ -104,12 +104,16 @@ class ResUsers(models.Model):
return (self.env.cr.dbname, values.get('login'), values.get('password'))
@api.model
+ def _get_signup_invitation_scope(self):
+ return self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b')
+
+ @api.model
def _signup_create_user(self, values):
""" signup a new user using the template user """
# check that uninvited users may sign up
if 'partner_id' not in values:
- if self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') != 'b2c':
+ if self._get_signup_invitation_scope() != 'b2c':
raise SignupError(_('Signup is not allowed for uninvited users'))
return self._create_user_from_template(values)
diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py
index 08c1d8a67a9..463ec49af7c 100644
--- a/addons/website/__manifest__.py
+++ b/addons/website/__manifest__.py
@@ -27,7 +27,9 @@
'views/website_views.xml',
'views/res_config_settings_views.xml',
'views/ir_actions_views.xml',
+ 'views/ir_attachment_views.xml',
'views/res_users_views.xml',
+ 'views/res_partner_views.xml',
'wizard/base_language_install_views.xml',
],
'demo': [
diff --git a/addons/website/controllers/backend.py b/addons/website/controllers/backend.py
index e5a2ecfa87e..9932843ff3a 100644
--- a/addons/website/controllers/backend.py
+++ b/addons/website/controllers/backend.py
@@ -8,7 +8,7 @@ from odoo.http import request
class WebsiteBackend(http.Controller):
@http.route('/website/fetch_dashboard_data', type="json", auth='user')
- def fetch_dashboard_data(self, date_from, date_to):
+ def fetch_dashboard_data(self, website_id, date_from, date_to):
has_group_system = request.env.user.has_group('base.group_system')
has_group_designer = request.env.user.has_group('website.group_website_designer')
dashboard_data = {
@@ -21,6 +21,13 @@ class WebsiteBackend(http.Controller):
'visits': {},
}
}
+
+ current_website = request.env['website'].browse(website_id) if website_id else request.env['website'].get_current_website()
+ dashboard_data['websites'] = request.env['website'].search_read([], ['id', 'name'])
+ for website in dashboard_data['websites']:
+ if website['id'] == current_website.id:
+ website['selected'] = True
+
if has_group_designer:
config = request.env['res.config.settings'].sudo().create({})
if config.has_google_analytics_dashboard:
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 5689b37352c..5287d480d71 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -81,6 +81,11 @@ class Website(Home):
raise request.not_found()
+ @http.route('/website/set_session_website', type='json', auth="user")
+ def set_session_website(self, website_id):
+ request.env['website'].browse(int(website_id) if website_id else False)._fix_to_session()
+ return True
+
#------------------------------------------------------
# Login - overwrite of the web login so that regular users are redirected to the backend
# while portal users are redirected to the frontend by default
@@ -221,7 +226,7 @@ class Website(Home):
# default sortby order
sort_order = searchbar_sortings.get(sortby, 'name')['order']
- domain = ['|', ('website_ids', 'in', request.website.id), ('website_ids', '=', False)]
+ domain = ['|', ('website_id', '=', False), ('website_id', '=', request.website.id)]
if search:
domain += ['|', ('name', 'ilike', search), ('url', 'ilike', search)]
@@ -236,6 +241,9 @@ class Website(Home):
)
pages = Page.search(domain, order=sort_order, limit=50, offset=pager['offset'])
+ # only include pages if they are the most specific one available
+ pages = pages.filtered(pages._is_most_specific_page)
+
values = {
'pager': pager,
'pages': pages,
diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml
index cc82e1d40ee..2c5c131f5e7 100644
--- a/addons/website/data/website_data.xml
+++ b/addons/website/data/website_data.xml
@@ -11,7 +11,6 @@
<record id="main_menu" model="website.menu">
<field name="name">Top Menu</field>
- <field name="website_id" ref="default_website"/>
</record>
<record id="menu_homepage" model="website.menu">
@@ -19,7 +18,6 @@
<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">
@@ -27,7 +25,6 @@
<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 -->
@@ -579,4 +576,9 @@
<field name="url">/website/static/src/img/snippets_demo/s_team_member_4.png</field>
</record>
</data>
+ <data>
+ <record id="group_multi_website" model="res.groups">
+ <field name="name">Multi-website</field>
+ </record>
+ </data>
</odoo>
diff --git a/addons/website/data/website_demo.xml b/addons/website/data/website_demo.xml
index 6c1be81c3aa..71e88ddd582 100644
--- a/addons/website/data/website_demo.xml
+++ b/addons/website/data/website_demo.xml
@@ -131,53 +131,6 @@ response = request.render("website.template_partner_comment", {
<field name="domain">0.0.0.0</field>
</record>
- <record id="website2_homepage" model="ir.ui.view">
- <field name="name">Home</field>
- <field name="type">qweb</field>
- <field name="key">website2.homepage</field>
- <field name="arch" type="xml">
- <t name="Home" priority="29" t-name="website2.homepage">
- <t t-call="website.layout">
- <div id="wrap" class="oe_structure oe_empty">
- <div class="carousel slide mb32" id="myCarousel0" style="height: 320px;">
- <ol class="carousel-indicators d-none">
- <li class="active" data-slide-to="0" data-target="#myCarousel0"/>
- </ol>
- <div class="carousel-inner">
- <div class="carousel-item image_text oe_img_bg active" style="background-image: url(http://0.0.0.0:8069/web/image/website.s_background_image_11);">
- <div class="container">
- <div class="row content">
- <div class="carousel-content col-lg-6 col-md-12">
- <h2>Homepage 0.0.0.0</h2>
- <h3>Click to customize this text</h3>
- <p>
- <a role="button" class="btn btn-success btn-large" href="/contactus">Contact us</a>
- </p>
- </div>
- <span class="carousel-img col-lg-6 d-none d-lg-block"> </span>
- </div>
- </div>
- </div>
- </div>
- <div class="carousel-control-prev d-none" data-slide="prev" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%" role="img" aria-label="Previous" title="Previous">
- <i class="fa fa-chevron-left"/>
- </div>
- <div class="carousel-control-next d-none" data-slide="next" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%" role="img" aria-label="Next" title="Next">
- <i class="fa fa-chevron-right"/>
- </div>
- </div>
- </div>
- </t>
- </t>
- </field>
- </record>
- <record id="website2_homepage_page" model="website.page">
- <field name="website_published">True</field>
- <field name="url">/</field>
- <field name="view_id" ref="website2_homepage" />
- <field name="website_ids" eval="[(4, ref('website2'))]" />
- </record>
-
<record id="website2_contactus" model="ir.ui.view">
<field name="name">Contact Us</field>
<field name="type">qweb</field>
@@ -216,29 +169,16 @@ response = request.render("website.template_partner_comment", {
<field name="website_published">True</field>
<field name="url">/contactus</field>
<field name="view_id" ref="website2_contactus" />
- <field name="website_ids" eval="[(4, ref('website2'))]" />
+ <field name="website_id" eval="ref('website2')" />
</record>
<!-- Menu & Homepage -->
<record id="website2" model="website">
- <field name="homepage_id" ref="website2_homepage_page" />
- </record>
- <record id="website2_main_menu" model="website.menu">
- <field name="name">Top Menu</field>
- <field name="website_id" ref="website2"/>
- </record>
- <record id="website2_menu_homepage" model="website.menu">
- <field name="name">Home</field>
- <field name="url">/</field>
- <field name="parent_id" ref="website.website2_main_menu"/>
- <field name="sequence" type="int">10</field>
- <field name="website_id" ref="website2"/>
- <field name="page_id" ref="website2_homepage_page" />
</record>
<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.website2_main_menu"/>
+ <field name="parent_id" ref="website.main_menu"/>
<field name="sequence" type="int">60</field>
<field name="website_id" ref="website2"/>
<field name="page_id" ref="website2_contactus_page" />
diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py
index 263c44b9e27..8fa63d63d09 100644
--- a/addons/website/models/__init__.py
+++ b/addons/website/models/__init__.py
@@ -10,5 +10,6 @@ from . import website
from . import ir_ui_view
from . import res_company
from . import res_partner
+from . import res_users
from . import res_config_settings
from . import res_users
diff --git a/addons/website/models/ir_attachment.py b/addons/website/models/ir_attachment.py
index a1f9df5dad1..7bbcea66607 100644
--- a/addons/website/models/ir_attachment.py
+++ b/addons/website/models/ir_attachment.py
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
from odoo import api, fields, models
+_logger = logging.getLogger(__name__)
class Attachment(models.Model):
@@ -14,3 +16,50 @@ class Attachment(models.Model):
@api.model
def get_serving_groups(self):
return super(Attachment, self).get_serving_groups() + ['website.group_website_designer']
+
+ key = fields.Char(help='Technical field used to resolve multiple attachments in a multi-website environment.')
+ theme_id = fields.Many2one('ir.module.module')
+
+ @api.multi
+ def unlink(self):
+ self |= self.search([('key', 'in', self.filtered('key').mapped('key'))])
+ return super(Attachment, self).unlink()
+
+ @api.multi
+ def _get_theme_specific_attachment(self, theme_name):
+ self.ensure_one()
+ attachment = self
+ module_being_updated = self.env['ir.module.module'].search([('name', '=', theme_name)])
+ xml_id = self.env['ir.model.data'].search([('model', '=', 'ir.attachment'), ('res_id', '=', attachment.id)])
+ if xml_id and xml_id.module != theme_name:
+ _logger.info('%s is updating attachment %s (ID: %s)', theme_name, xml_id.complete_name, attachment.id)
+
+ # check if a previously copied attachment for this theme already exists
+ theme_specific_attachment = self.env['ir.attachment'].search([('key', '=', attachment.key), ('theme_id', '=', module_being_updated.id)])
+ if theme_specific_attachment:
+ attachment = theme_specific_attachment
+ _logger.info('diverting write to %s (ID: %s)', attachment.name, attachment.id)
+ else:
+ attachment.with_context(no_cow=True).key = xml_id.complete_name
+ attachment = attachment.copy({
+ 'theme_id': module_being_updated.id,
+ 'key': xml_id.complete_name,
+ })
+ _logger.info('created new theme-specific attachment %s (ID: %s)', attachment.name, attachment.id)
+
+ return attachment
+
+ @api.multi
+ def write(self, vals):
+ if not self._context.get('no_cow'):
+ for attachment in self:
+ currently_updating = self._context.get('install_mode_data', {}).get('module', '')
+ if currently_updating.startswith('theme_'):
+ attachment = attachment._get_theme_specific_attachment(currently_updating)
+
+ super(Attachment, attachment).write(vals)
+ else:
+ super(Attachment, self).write(vals)
+
+ return True
+>>>>>>> [IMP] website,*: support multiple websites
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index 10250e2a756..3956a6382fc 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -117,9 +117,16 @@ class Http(models.AbstractModel):
@classmethod
def _serve_page(cls):
req_page = request.httprequest.path
+ page_domain = [('url', '=', req_page), '|', ('website_id', '=', False), ('website_id', '=', request.website.id)]
- domain = [('url', '=', req_page), '|', ('website_ids', 'in', request.website.id), ('website_ids', '=', False)]
- pages = request.env['website.page'].search(domain)
+ published_domain = page_domain + [('website_published', '=', True)]
+ pages = request.env['website.page'].search(published_domain)
+
+ if not pages:
+ # Since there are no published pages, try to find a page
+ # that could potentially be published.
+ unpublished_domain = page_domain + [('website_published', '=', False)]
+ pages = request.env['website.page'].search(unpublished_domain)
if not request.website.is_publisher():
pages = pages.filtered('is_visible')
@@ -239,7 +246,7 @@ class Http(models.AbstractModel):
env = env or request.env
obj = None
if xmlid:
- obj = env.ref(xmlid, False)
+ obj = cls._xmlid_to_obj(env, xmlid)
elif id and model in env:
obj = env[model].browse(int(id))
if obj and 'website_published' in obj._fields:
@@ -251,6 +258,15 @@ class Http(models.AbstractModel):
default_mimetype=default_mimetype, access_token=access_token, share_id=share_id, share_token=share_token,
force_ext=force_ext, env=env)
+ @classmethod
+ def _xmlid_to_obj(cls, env, xmlid):
+ website_id = env['website'].get_current_website()
+ if website_id and website_id.installed_theme_id:
+ obj = env['ir.attachment'].search([('key', '=', xmlid), ('theme_id', '=', website_id.installed_theme_id.id)])
+ if obj:
+ return obj[0]
+
+ return super(Http, cls)._xmlid_to_obj(env, xmlid)
class ModelConverter(ModelConverter):
diff --git a/addons/website/models/ir_rule.py b/addons/website/models/ir_rule.py
new file mode 100644
index 00000000000..2a90dbfacfa
--- /dev/null
+++ b/addons/website/models/ir_rule.py
@@ -0,0 +1,12 @@
+# coding: utf-8
+from odoo import api, models
+
+
+class IrRule(models.Model):
+ _inherit = 'ir.rule'
+
+ @api.model
+ def _eval_context(self):
+ res = super(IrRule, self)._eval_context()
+ res['website_id'] = self._context.get('website_id')
+ return res
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 9a793b84fe6..8a8e5a3f8f3 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -4,9 +4,10 @@
import logging
from itertools import groupby
-from odoo import api, fields, models
+from odoo import api, fields, models, _
from odoo import tools
from odoo.addons.http_routing.models.ir_http import url_for
+from odoo.osv import expression
from odoo.http import request
from odoo.tools import pycompat
@@ -22,6 +23,7 @@ class View(models.Model):
website_id = fields.Many2one('website', ondelete='cascade', string="Website")
page_ids = fields.One2many('website.page', compute='_compute_page_ids', store=False)
first_page_id = fields.Many2one('website.page', string='Website Page', help='First page linked to this view', compute='_compute_first_page_id')
+ theme_id = fields.Many2one('ir.module.module')
@api.one
def _compute_first_page_id(self):
@@ -34,12 +36,112 @@ class View(models.Model):
)
@api.multi
+ def write(self, vals):
+ '''COW for ir.ui.view. This way editing websites does not impact other
+ websites. Also this way newly created websites will only
+ contain the default views.
+ '''
+ if not self._context.get('no_cow'):
+ current_website_id = self._context.get('website_id')
+ for view in self:
+ currently_updating = self._context.get('install_mode_data', {}).get('module', '')
+ if 'theme_' in currently_updating:
+ current_website_id = False
+ view = view._get_theme_specific_view(currently_updating)
+
+ # if generic view in multi-website context
+ if current_website_id and not view.website_id:
+ new_website_specific_view = view.copy({'website_id': current_website_id})
+ view._create_website_specific_pages_for_view(new_website_specific_view,
+ view.env['website'].browse(current_website_id))
+
+ # trigger COW on inheriting views
+ for inherit_child in view.inherit_children_ids:
+ inherit_child.write({'inherit_id': new_website_specific_view.id})
+
+ new_website_specific_view.write(vals)
+ else:
+ super(View, view).write(vals)
+ else:
+ super(View, self).write(vals)
+
+ return True
+
+ @api.multi
def unlink(self):
+ '''This implements COU (copy-on-unlink). When deleting a generic page
+ website-specific pages will be created so only the current
+ website is affected.
+ '''
+ current_website_id = self._context.get('website_id')
+
+ if current_website_id and not self._context.get('no_cow'):
+ for view in self.filtered(lambda view: not view.website_id):
+ for website in self.env['website'].search([('id', '!=', current_website_id)]):
+ # 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)})
+
+ 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
@api.multi
+ def _get_theme_specific_view(self, theme_name):
+ self.ensure_one()
+ view = self
+ module_being_updated = self.env['ir.module.module'].search([('name', '=', theme_name)])
+ xml_id = self.env['ir.model.data'].search([('model', '=', 'ir.ui.view'), ('res_id', '=', view.id)])
+ if xml_id and xml_id.module != theme_name:
+ _logger.info('%s is updating view %s (ID: %s)', theme_name, xml_id.complete_name, view.id)
+
+ # check if a previously copied view for this theme already exists
+ theme_specific_view = self.env['ir.ui.view'].search([('key', '=', view.key), ('theme_id', '=', module_being_updated.id)])
+ if theme_specific_view:
+ view = theme_specific_view
+ _logger.info('diverting write to %s (ID: %s)', view.name, view.id)
+ else:
+ view = view.copy({'theme_id': module_being_updated.id})
+ _logger.info('created new theme-specific view %s (ID: %s)', view.name, view.id)
+
+ return view
+
+ def _create_website_specific_pages_for_view(self, new_view, website):
+ for page in self.page_ids:
+ # create new pages for this view
+ 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):
+ '''Make this only return most specific views for website.'''
+ # get_related_views can be called through website=False routes
+ # (e.g. /web_editor/get_assets_editor_resources), so website
+ # dispatch_parameters may not be added. Manually set
+ # website_id.
+ self = self.with_context(website_id=self.env['website'].get_current_website().id)
+ views = super(View, self).get_related_views(key, bundles=bundles)
+ current_website_id = self._context.get('website_id')
+ most_specific_views = self.env['ir.ui.view']
+
+ if not current_website_id:
+ return views
+
+ for view in views:
+ if view.website_id and view.website_id.id == current_website_id:
+ most_specific_views |= view
+ elif not view.website_id and not any(view.key == view2.key and view2.website_id and view2.website_id.id == current_website_id for view2 in views):
+ most_specific_views |= view
+
+ return most_specific_views
+
+ @api.multi
def _sort_suitability_key(self):
""" Key function to sort views by descending suitability
Suitability of a view is defined as follow:
@@ -80,15 +182,47 @@ class View(models.Model):
return view_id
@api.model
+ def _get_inheriting_views_arch_website(self, view_id):
+ return self.env['website'].browse(self._context.get('website_id'))
+
+ @api.model
def _get_inheriting_views_arch_domain(self, view_id, model):
domain = super(View, self)._get_inheriting_views_arch_domain(view_id, model)
- return ['|', ('website_id', '=', False), ('website_id', '=', self.env.context.get('website_id'))] + domain
+ current_website = self._get_inheriting_views_arch_website(view_id)
+
+ website_views_domain = [('theme_id', '=', False), '|', ('website_id', '=', False), ('website_id', '=', current_website.id)]
+ # when rendering for the website we have to include inactive views
+ # we will prefer inactive website-specific views over active generic ones
+ if current_website:
+ domain = [leaf for leaf in domain if 'active' not in leaf]
+ if current_website.theme_ids:
+ theme_views_domain = [('theme_id', 'in', current_website.theme_ids.ids)]
+ website_views_domain = expression.OR([website_views_domain, theme_views_domain])
+
+ return expression.AND([website_views_domain, domain])
+
+ @api.model
+ def get_inheriting_views_arch(self, view_id, model):
+ if not self._context.get('website_id'):
+ return super(View, self).get_inheriting_views_arch(view_id, model)
+
+ inheriting_views = super(View, self.with_context(active_test=False)).get_inheriting_views_arch(view_id, model)
+
+ # prefer inactive website-specific views over active generic ones
+ inheriting_views = self.browse([view[1] for view in inheriting_views]).filter_duplicate().filtered('active')
+
+ return [(view.arch, view.id) for view in inheriting_views]
@api.model
@tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',))
def get_view_id(self, xml_id):
if 'website_id' in self._context and not isinstance(xml_id, pycompat.integer_types):
- domain = [('key', '=', xml_id), '|', ('website_id', '=', self._context['website_id']), ('website_id', '=', False)]
+ current_website = self.env['website'].browse(self._context.get('website_id'))
+ key_domain = [('key', '=', xml_id)]
+ theme_views_domain = [('theme_id', 'in', current_website.theme_ids.ids)]
+ website_views_domain = [('theme_id', '=', False), '|', ('website_id', '=', False), ('website_id', '=', current_website.id)]
+ domain = expression.AND([expression.OR([theme_views_domain, website_views_domain]), key_domain])
+
view = self.search(domain, order='website_id', limit=1)
if not view:
_logger.warning("Could not find view object with xml_id '%s'", xml_id)
@@ -125,6 +259,7 @@ class View(models.Model):
qcontext = super(View, self)._prepare_qcontext()
if request and getattr(request, 'is_frontend', False):
+ Website = self.env['website']
editable = request.website.is_publisher()
translatable = editable and self._context.get('lang') != request.env['ir.http']._get_default_lang().code
editable = not translatable and editable
@@ -132,6 +267,17 @@ class View(models.Model):
if 'main_object' not in qcontext:
qcontext['main_object'] = self
+ domain_based_info = {'website_id': '', 'name': _('Domain Based')}
+ force_website_id = request.session.get('force_website_id', False)
+ if force_website_id:
+ selected_website = Website.browse(force_website_id)
+ qcontext['multi_website_selected_website'] = {'website_id': selected_website.id, 'name': selected_website.name}
+ else:
+ qcontext['multi_website_selected_website'] = domain_based_info
+
+ qcontext['multi_website_websites'] = [{'website_id': website.id, 'name': website.name} for website in Website.search([])]
+ qcontext['multi_website_websites'] += [domain_based_info]
+
qcontext.update(dict(
self._context.copy(),
website=request.website,
diff --git a/addons/website/models/res_company.py b/addons/website/models/res_company.py
index bc1e6fa5002..48b40c7a2f4 100644
--- a/addons/website/models/res_company.py
+++ b/addons/website/models/res_company.py
@@ -16,3 +16,19 @@ class Company(models.Model):
def google_map_link(self, zoom=8):
partner = self.sudo().partner_id
return partner and partner.google_map_link(zoom) or None
+
+ @api.multi
+ def _get_public_user(self):
+ self.ensure_one()
+ public_users = self.env.ref('base.group_public').with_context(active_test=False).users
+ public_users_for_website = public_users.filtered(lambda user: user.company_id == self)
+
+ if public_users_for_website:
+ return public_users_for_website[0]
+ else:
+ return self.env.ref('base.public_user').copy({
+ 'name': 'Public user for %s' % self.name,
+ 'login': 'public_company_%s' % self.id,
+ 'company_id': self.id,
+ 'company_ids': [(6, 0, [self.id])],
+ })
diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py
index f2b79b88479..3e8aaf58a80 100644
--- a/addons/website/models/res_config_settings.py
+++ b/addons/website/models/res_config_settings.py
@@ -16,6 +16,9 @@ class ResConfigSettings(models.TransientModel):
website_id = fields.Many2one('website', string="website",
default=_default_website, required=True, ondelete='cascade')
website_name = fields.Char('Website Name', related='website_id.name')
+ website_domain = fields.Char('Website Domain', related='website_id.domain')
+ website_country_group_ids = fields.Many2many(related='website_id.country_group_ids')
+ website_company_id = fields.Many2one(related='website_id.company_id', string='Website Company')
language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang')
language_count = fields.Integer(string='Number of languages', compute='_compute_language_count', readonly=True)
website_default_lang_id = fields.Many2one(
@@ -34,6 +37,7 @@ class ResConfigSettings(models.TransientModel):
cdn_filters = fields.Text(related='website_id.cdn_filters')
module_website_version = fields.Boolean("A/B Testing")
module_website_links = fields.Boolean(string="Link Trackers")
+ auth_signup_uninvited = fields.Selection(string="Customer Account", related='website_id.auth_signup_uninvited')
favicon = fields.Binary('Favicon', related='website_id.favicon')
# Set as global config parameter since methods using it are not website-aware. To be changed
@@ -42,10 +46,15 @@ class ResConfigSettings(models.TransientModel):
has_google_analytics = fields.Boolean("Google Analytics", config_parameter='website.has_google_analytics')
has_google_analytics_dashboard = fields.Boolean("Embedded Google Analytics", config_parameter='website.has_google_analytics_dashboard')
has_google_maps = fields.Boolean("Google Maps", config_parameter='website.has_google_maps')
- auth_signup_uninvited = fields.Selection([
- ('b2b', 'On invitation (B2B)'),
- ('b2c', 'Free sign up (B2C)'),
- ], string='Customer Account', default='b2b', config_parameter='auth_signup.invitation_scope')
+
+ social_twitter = fields.Char(related='website_id.social_twitter')
+ social_facebook = fields.Char(related='website_id.social_facebook')
+ social_github = fields.Char(related='website_id.social_github')
+ social_linkedin = fields.Char(related='website_id.social_linkedin')
+ social_youtube = fields.Char(related='website_id.social_youtube')
+ social_googleplus = fields.Char(related='website_id.social_googleplus')
+
+ group_multi_website = fields.Boolean("Multi-website", implied_group="website.group_multi_website")
@api.onchange('has_google_analytics')
def onchange_has_google_analytics(self):
@@ -83,3 +92,11 @@ class ResConfigSettings(models.TransientModel):
action['res_id'] = literal_eval(self.env['ir.config_parameter'].sudo().get_param('base.template_portal_user_id', 'False'))
action['views'] = [[self.env.ref('base.view_users_form').id, 'form']]
return action
+
+ def website_go_to(self):
+ self.website_id._fix_to_session()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/',
+ 'target': 'self',
+ }
diff --git a/addons/website/models/res_partner.py b/addons/website/models/res_partner.py
index d3a3b1bc9f9..2329e0ff1c8 100644
--- a/addons/website/models/res_partner.py
+++ b/addons/website/models/res_partner.py
@@ -3,7 +3,7 @@
import werkzeug
-from odoo import api, models
+from odoo import api, fields, models
def urlplus(url, params):
@@ -11,9 +11,10 @@ def urlplus(url, params):
class Partner(models.Model):
-
_inherit = "res.partner"
+ website_id = fields.Many2one('website', string='Registration Website')
+
@api.multi
def google_map_img(self, zoom=8, width=298, height=298):
google_maps_api_key = self.env['ir.config_parameter'].sudo().get_param('google_maps_api_key')
diff --git a/addons/website/models/res_users.py b/addons/website/models/res_users.py
index d8eb61d9e7c..31fada61a7c 100644
--- a/addons/website/models/res_users.py
+++ b/addons/website/models/res_users.py
@@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
-from odoo import api, fields, models
+from odoo import api, fields, models, _
+from odoo.exceptions import ValidationError
+
+_logger = logging.getLogger(__name__)
class ResUsers(models.Model):
@@ -11,10 +15,46 @@ class ResUsers(models.Model):
selection=lambda self: self._get_group_selection('base.module_category_website'),
string='Website Editor', compute='_compute_groups_id', inverse='_inverse_groups_id',
category_xml_id='base.module_category_website')
+ has_group_multi_website = fields.Boolean(
+ 'Multi-Website',
+ compute='_compute_groups_id', inverse='_inverse_groups_id',
+ group_xml_id='website.group_multi_website')
+ _sql_constraints = [
+ # this is done in Python because a SQL constraint like UNIQUE
+ # (login, website_id) allows ('abc', NULL) and
+ # ('abc', NULL) to coexist because of how SQL handles NULLs.
+ ('login_key', 'CHECK (1=1)', 'You can not have two users with the same login!')
+ ]
+
@api.multi
def _has_unsplash_key_rights(self):
self.ensure_one()
if self.has_group('website.group_website_designer'):
return True
return super(ResUsers, self)._has_unsplash_key_rights()
+
+ @api.constrains('login', 'website_id')
+ def _check_login(self):
+ for user in self:
+ if self.search([('id', '!=', user.id), ('login', '=', user.login),
+ '|', ('website_id', '=', False),
+ ('website_id', '=', user.website_id.id)]):
+ raise ValidationError(_('You can not have two users with the same login!'))
+
+ @api.model
+ def _get_login_domain(self, login):
+ current_website_id = self.env['website'].get_current_website().id
+ multi_website_domain = ['|', ('website_id', '=', False), ('website_id', '=', current_website_id)]
+ return super(ResUsers, self)._get_login_domain(login) + multi_website_domain
+
+ @api.model
+ def _signup_create_user(self, values):
+ new_user = super(ResUsers, self)._signup_create_user(values)
+ new_user.website_id = self.env['website'].get_current_website()
+ return new_user
+
+ @api.model
+ def _get_signup_invitation_scope(self):
+ current_website = self.env['website'].get_current_website()
+ return current_website.auth_signup_uninvited or super(ResUsers, self)._get_signup_invitation_scope()
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 94d73420ccf..c0310d53d3d 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -15,6 +15,7 @@ from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.portal.controllers.portal import pager
from odoo.tools import pycompat
from odoo.http import request
+from odoo.osv import expression
from odoo.osv.expression import FALSE_DOMAIN
from odoo.tools.translate import _
@@ -44,26 +45,46 @@ class Website(models.Model):
def_lang = self.env['res.lang'].search([('code', '=', lang_code)], limit=1)
return def_lang.id if def_lang else self._active_languages()[0]
+ def _default_social_twitter(self):
+ return self.env.ref('base.main_company').social_twitter
+
+ def _default_social_facebook(self):
+ return self.env.ref('base.main_company').social_facebook
+
+ def _default_social_github(self):
+ return self.env.ref('base.main_company').social_github
+
+ def _default_social_linkedin(self):
+ return self.env.ref('base.main_company').social_linkedin
+
+ def _default_social_youtube(self):
+ return self.env.ref('base.main_company').social_youtube
+
+ def _default_social_googleplus(self):
+ return self.env.ref('base.main_company').social_googleplus
+
name = fields.Char('Website Name')
domain = fields.Char('Website Domain')
+ country_group_ids = fields.Many2many('res.country.group', 'website_country_group_rel', 'website_id', 'country_group_id',
+ string='Country Groups', help='Used when multiple websites have the same domain.')
company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.ref('base.main_company').id)
language_ids = fields.Many2many('res.lang', 'website_lang_rel', 'website_id', 'lang_id', 'Languages', default=_active_languages)
default_lang_id = fields.Many2one('res.lang', string="Default Language", default=_default_language, required=True)
default_lang_code = fields.Char(related='default_lang_id.code', string="Default language code", store=True)
auto_redirect_lang = fields.Boolean('Autoredirect Language', default=True, help="Should users be redirected to their browser's language")
- social_twitter = fields.Char(related="company_id.social_twitter")
- social_facebook = fields.Char(related="company_id.social_facebook")
- social_github = fields.Char(related="company_id.social_github")
- social_linkedin = fields.Char(related="company_id.social_linkedin")
- social_youtube = fields.Char(related="company_id.social_youtube")
- social_googleplus = fields.Char(related="company_id.social_googleplus")
+ social_twitter = fields.Char('Twitter Account', default=_default_social_twitter)
+ social_facebook = fields.Char('Facebook Account', default=_default_social_facebook)
+ social_github = fields.Char('GitHub Account', default=_default_social_github)
+ social_linkedin = fields.Char('LinkedIn Account', default=_default_social_linkedin)
+ social_youtube = fields.Char('Youtube Account', default=_default_social_youtube)
+ social_googleplus = fields.Char('Google+ Account', default=_default_social_googleplus)
google_analytics_key = fields.Char('Google Analytics Key')
google_management_client_id = fields.Char('Google Client ID')
google_management_client_secret = fields.Char('Google Client Secret')
- user_id = fields.Many2one('res.users', string='Public User', required=True, default=lambda self: self.env.ref('base.public_user').id)
+ user_id = fields.Many2one('res.users', string='Public User', required=True)
cdn_activated = fields.Boolean('Activate CDN for assets')
cdn_url = fields.Char('CDN Base URL', default='')
cdn_filters = fields.Text('CDN Filters', default=lambda s: '\n'.join(DEFAULT_CDN_FILTERS), help="URL matching those filters will be rewritten using the CDN Base URL")
@@ -71,29 +92,73 @@ class Website(models.Model):
menu_id = fields.Many2one('website.menu', compute='_compute_menu', string='Main Menu')
homepage_id = fields.Many2one('website.page', string='Homepage')
favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.")
+ 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)'),
+ ], 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', '=', website.id)], order='id', limit=1).id
+ website.menu_id = Menu.search([('parent_id', '=', False), ('website_id', 'in', (website.id, False))], order='id', limit=1).id
# cf. Wizard hack in website_views.xml
def noop(self, *args, **kwargs):
pass
+ @api.model
+ def create(self, vals):
+ if 'user_id' not in vals:
+ company = self.env['res.company'].browse(vals.get('company_id'))
+ vals['user_id'] = company._get_public_user().id if company else self.env.ref('base.public_user').id
+
+ res = super(Website, self).create(vals)
+ res._bootstrap_homepage()
+ return res
+
@api.multi
def write(self, values):
self._get_languages.clear_cache(self)
+ if 'company_id' in values and 'user_id' not in values:
+ company = self.env['res.company'].browse(values['company_id'])
+ values['user_id'] = company._get_public_user().id
+
result = super(Website, self).write(values)
if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values:
# invalidate the caches from static node at compile time
self.env['ir.qweb'].clear_caches()
return result
+ @api.onchange('company_id')
+ def _onchange_company_id(self):
+ self.social_twitter = self.company_id.social_twitter
+ self.social_facebook = self.company_id.social_facebook
+ self.social_github = self.company_id.social_github
+ self.social_linkedin = self.company_id.social_linkedin
+ self.social_youtube = self.company_id.social_youtube
+ self.social_googleplus = self.company_id.social_googleplus
+
#----------------------------------------------------------
# Page Management
#----------------------------------------------------------
+ def _bootstrap_homepage(self):
+ standard_homepage = self.env.ref('website.homepage', raise_if_not_found=False)
+ if not standard_homepage:
+ return
+
+ new_homepage_view = '''<t name="Homepage" t-name="website.homepage%s">
+ <t t-call="website.layout">
+%s
+ </t>
+</t>''' % (self.id, self.env['ir.ui.view'].render_template('website.default_homepage', values={'website': self}).decode())
+ standard_homepage.with_context(website_id=self.id).arch_db = new_homepage_view
+
+ self.homepage_id = self.env['website.page'].search([('website_id', '=', self.id),
+ ('key', '=', standard_homepage.key)])
+
@api.model
def new_page(self, name=False, add_menu=False, template='website.default_page', ispage=True, namespace=None):
""" Create a new website page, and assign it a xmlid based on the given one
@@ -130,8 +195,8 @@ class Website(models.Model):
if ispage:
page = self.env['website.page'].create({
'url': page_url,
- 'website_ids': [(6, None, [self.get_current_website().id])],
- 'view_id': view.id
+ 'website_id': self.get_current_website().id,
+ 'view_id': view.id,
})
result['view_id'] = view.id
if add_menu:
@@ -154,7 +219,7 @@ class Website(models.Model):
"""
website_id = self.get_current_website().id
inc = 0
- domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
+ domain_static = ['|', ('website_id', '=', False), ('website_id', '=', website_id)]
page_temp = page_url
while self.env['website.page'].with_context(active_test=False).sudo().search([('url', '=', page_temp)] + domain_static):
inc += 1
@@ -177,7 +242,7 @@ class Website(models.Model):
#Look for unique key
key_copy = string
inc = 0
- domain_static = ['|', ('website_ids', '=', False), ('website_ids', 'in', website_id)]
+ domain_static = ['|', ('website_id', '=', False), ('website_id', '=', website_id)]
while self.env['website.page'].with_context(active_test=False).sudo().search([('key', '=', key_copy)] + domain_static):
inc += 1
key_copy = string + (inc and "-%s" % inc or "")
@@ -208,7 +273,7 @@ class Website(models.Model):
# search for website_page with link
website_page_search_dom = [
- '|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('view_id.arch_db', 'ilike', url)
+ '|', ('website_id', '=', False), ('website_id', '=', website_id), ('view_id.arch_db', 'ilike', url)
]
pages = self.env['website.page'].search(website_page_search_dom)
page_key = _('Page')
@@ -276,7 +341,7 @@ class Website(models.Model):
# search for website_page with link
website_page_search_dom = [
- '|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('view_id.arch_db', 'ilike', key),
+ '|', ('website_id', '=', False), ('website_id', '=', website_id), ('view_id.arch_db', 'ilike', key),
('id', '!=', page.id),
]
pages = self.env['website.page'].search(website_page_search_dom)
@@ -376,19 +441,38 @@ class Website(models.Model):
@api.model
def get_current_website(self):
+ if request and request.session.get('force_website_id'):
+ return self.browse(request.session['force_website_id'])
+
domain_name = request and request.httprequest.environ.get('HTTP_HOST', '').split(':')[0] or None
- website_id = self._get_current_website_id(domain_name)
+
+ country = request.session.geoip.get('country_code') if request and request.session.geoip else False
+ country_id = False
+ if country:
+ country_id = request.env['res.country'].search([('code', '=', country)], limit=1).id
+
+ website_id = self._get_current_website_id(domain_name, country_id)
if request:
request.context = dict(request.context, website_id=website_id)
return self.browse(website_id)
- @tools.cache('domain_name')
- def _get_current_website_id(self, domain_name):
- """ Reminder : cached method should be return record, since they will use a closed cursor. """
- website = self.search([('domain', '=', domain_name)], limit=1)
- if not website:
- website = self.search([], limit=1)
- return website.id
+ @tools.cache('domain_name', 'country_id')
+ def _get_current_website_id(self, domain_name, country_id):
+ # sort on country_group_ids so that we fall back on a generic website (empty country_group_ids)
+ websites = self.search([('domain', '=', domain_name)]).sorted('country_group_ids')
+
+ if not websites:
+ return self.search([], limit=1).id
+ elif len(websites) == 1:
+ return websites.id
+ else: # > 1 website with the same domain
+ country_specific_websites = websites.filtered(lambda website: country_id in website.country_group_ids.mapped('country_ids').ids)
+ return country_specific_websites[0].id if country_specific_websites else websites[0].id
+
+ def _fix_to_session(self):
+ # call on empty recordset to clear
+ if request:
+ request.session['force_website_id'] = self.id
@api.model
def is_publisher(self):
@@ -545,7 +629,7 @@ class Website(models.Model):
@api.multi
def get_website_pages(self, domain=[], order='name', limit=None):
- domain += ['|', ('website_ids', 'in', self.get_current_website().id), ('website_ids', '=', False)]
+ domain += ['|', ('website_id', '=', False), ('website_id', '=', self.get_current_website().id)]
pages = request.env['website.page'].search(domain, order='name', limit=limit)
return pages
@@ -599,10 +683,49 @@ class WebsitePublishedMixin(models.AbstractModel):
_name = "website.published.mixin"
- website_published = fields.Boolean('Visible in Website', copy=False)
+ website_published = fields.Boolean('Visible on current website',
+ compute='_compute_website_published',
+ inverse='_inverse_website_published',
+ search='_search_website_published') # todo jov evaluate places where this is used, we can probably replace some with is_published
+ is_published = fields.Boolean('Is published')
+ website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.')
website_url = fields.Char('Website URL', compute='_compute_website_url', help='The full URL to access the document through the website.')
@api.multi
+ @api.depends('is_published', 'website_id')
+ def _compute_website_published(self):
+ current_website_id = self._context.get('website_id')
+ for record in self:
+ if current_website_id:
+ record.website_published = record.is_published and (not record.website_id or record.website_id.id == current_website_id)
+ else:
+ record.website_published = record.is_published
+
+ @api.multi
+ def _inverse_website_published(self):
+ current_website_id = self._context.get('website_id')
+ for record in self:
+ record.is_published = record.website_published
+ if record.website_published and current_website_id:
+ record.write({'website_id': current_website_id})
+
+ def _search_website_published(self, operator, value):
+ if not isinstance(value, bool) or operator not in ('=', '!='):
+ logger.warning('unsupported search on website_published: %s, %s', operator, value)
+ return [()]
+
+ if operator in expression.NEGATIVE_TERM_OPERATORS:
+ value = not value
+
+ current_website_id = self._context.get('website_id')
+ if current_website_id:
+ is_published = [('is_published', '=', True)]
+ on_current_website = expression.OR([[('website_id', '=', False)], [('website_id', '=', current_website_id)]])
+ return (['!'] if value is False else []) + expression.AND([is_published, on_current_website])
+ else: # should be in the backend, return things that are published anywhere
+ return [('is_published', '=', value)]
+
+ @api.multi
def _compute_website_url(self):
for record in self:
record.website_url = '#'
@@ -627,9 +750,9 @@ class Page(models.Model):
_inherits = {'ir.ui.view': 'view_id'}
_inherit = 'website.published.mixin'
_description = 'Page'
+ _order = 'website_id'
url = fields.Char('Page URL')
- website_ids = fields.Many2many('website', string='Websites')
view_id = fields.Many2one('ir.ui.view', string='View', required=True, ondelete="cascade")
website_indexed = fields.Boolean('Page Indexed', default=True)
date_publish = fields.Datetime('Publishing Date')
@@ -642,6 +765,9 @@ class Page(models.Model):
header_overlay = fields.Boolean()
header_color = fields.Char()
+ # don't use mixin website_id but use website_id on ir.ui.view instead
+ website_id = fields.Many2one(related='view_id.website_id', store=True)
+
@api.one
def _compute_homepage(self):
self.is_homepage = self == self.env['website'].get_current_website().homepage_id
@@ -660,10 +786,20 @@ class Page(models.Model):
def _compute_visible(self):
self.is_visible = self.website_published and (not self.date_publish or self.date_publish < fields.Datetime.now())
+ @api.multi
+ def _is_most_specific_page(self, page_to_test):
+ '''This will test if page_to_test is the most specific page in self.'''
+ pages_for_url = self.filtered(lambda page: page.url == page_to_test.url)
+
+ # this works because pages are _order'ed by website_id
+ most_specific_page = pages_for_url[0]
+
+ return most_specific_page == page_to_test
+
@api.model
def get_page_info(self, id, website_id):
- domain = ['|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('id', '=', id)]
- item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage'], limit=1)
+ domain = ['|', ('website_id', '=', False), ('website_id', '=', website_id), ('id', '=', id)]
+ item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage', 'website_id'], limit=1)
return item
@api.multi
@@ -709,11 +845,15 @@ class Page(models.Model):
'website_id': website.id,
})
- page.write({
+ # Edits via the page manager shouldn't trigger the COW
+ # mechanism and generate new pages. The user manages page
+ # visibility manually with is_published here.
+ page.with_context(no_cow=True).write({
'key': page_key,
'name': data['name'],
'url': url,
- 'website_published': data['website_published'],
+ 'is_published': data['website_published'],
+ 'website_id': website.id if data['restrict_to_current_website'] else False,
'website_indexed': data['website_indexed'],
'date_publish': data['date_publish'] or None,
'is_homepage': data['is_homepage'],
@@ -733,17 +873,18 @@ class Page(models.Model):
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
- view = self.env['ir.ui.view'].browse(self.view_id.id)
- # website.page's ir.ui.view should have a different key than the one it
- # 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)'))})
- default = {
- 'name': '%s %s' % (self.name, _('(copy)')),
- 'url': self.env['website'].get_unique_path(self.url),
- 'view_id': new_view.id,
- }
+ if default and not default.get('view_id'):
+ view = self.env['ir.ui.view'].browse(self.view_id.id)
+ # website.page's ir.ui.view should have a different key than the one it
+ # 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)'))})
+ default = {
+ 'name': '%s %s' % (self.name, _('(copy)')),
+ 'url': self.env['website'].get_unique_path(self.url),
+ 'view_id': new_view.id,
+ }
return super(Page, self).copy(default=default)
@api.model
@@ -758,47 +899,30 @@ class Page(models.Model):
if menu:
# If the page being cloned has a menu, clone it too
new_menu = menu.copy()
- new_menu.write({'url': new_page.url, 'name': '%s %s' % (menu.name, _('(copy)')), 'page_id': new_page.id})
+ new_menu.write({'url': new_page.url, 'name': '%s %s' % (menu.name, _('(copy)')), 'page_id': new_page.id})
return new_page.url + '?enable_editor=1'
@api.multi
def unlink(self):
- """ When a website_page is deleted, the ORM does not delete its ir_ui_view.
- So we got to delete it ourself, but only if the ir_ui_view is not used by another website_page.
- """
- # Handle it's ir_ui_view
+ # When a website_page is deleted, the ORM does not delete its
+ # ir_ui_view. So we got to delete it ourself, but only if the
+ # ir_ui_view is not used by another website_page.
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)]
)
- if len(pages_linked_to_iruiview) == 0:
+ 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
- self.env['ir.ui.view'].search([('id', '=', self.view_id.id)]).unlink()
- # And then delete the website_page itself
+ page.view_id.unlink()
return super(Page, self).unlink()
- @api.model
- def delete_page(self, page_id):
- """ Delete a page, given its identifier
- :param page_id : website.page identifier
- """
- # If we are deleting a page (that could possibly be a menu with a page)
- page = self.env['website.page'].browse(int(page_id))
- if page:
- # Check if it is a menu with a page and also delete menu if so
- menu = self.env['website.menu'].search([('page_id', '=', page.id)], limit=1)
- if menu:
- menu.unlink()
- page.unlink()
-
@api.multi
def write(self, vals):
if 'url' in vals and not vals['url'].startswith('/'):
vals['url'] = '/' + vals['url']
- result = super(Page, self).write(vals)
- return result
+ return super(Page, self).write(vals)
class Menu(models.Model):
@@ -815,15 +939,47 @@ class Menu(models.Model):
name = fields.Char('Menu', required=True, translate=True)
url = fields.Char('Url', default='')
- page_id = fields.Many2one('website.page', 'Related Page')
+ page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade')
new_window = fields.Boolean('New Window')
sequence = fields.Integer(default=_default_sequence)
- website_id = fields.Many2one('website', 'Website') # TODO: support multiwebsite once done for ir.ui.views
+ website_id = fields.Many2one('website', 'Website')
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
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.
+ '''
+ 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)
+
@api.one
def _compute_visible(self):
visible = True
@@ -846,6 +1002,21 @@ 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):
@@ -862,7 +1033,7 @@ class Menu(models.Model):
children=[],
is_homepage=is_homepage,
)
- for child in node.child_id:
+ for child in node.get_children_for_current_website():
menu_node['children'].append(make_tree(child))
return menu_node
if menu_id:
diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js
index 9ece194ab1e..393709b91db 100644
--- a/addons/website/static/src/js/backend/dashboard.js
+++ b/addons/website/static/src/js/backend/dashboard.js
@@ -64,6 +64,7 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
return this._rpc({
route: '/website/fetch_dashboard_data',
params: {
+ website_id: this.website_id || false,
date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
},
@@ -72,6 +73,7 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
self.dashboards_data = result.dashboards;
self.currency_id = result.currency_id;
self.groups = result.groups;
+ self.websites = result.websites;
});
},
@@ -245,6 +247,16 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
},
+ on_website_button: function(website_id) {
+ var self = this;
+ this.website_id = website_id;
+ $.when(this.fetch_data()).then(function() {
+ self.$('.o_website_dashboard').empty();
+ self.render_dashboards();
+ self.render_graphs();
+ });
+ },
+
on_reverse_breadcrumb: function() {
var self = this;
web_client.do_push_state({});
@@ -293,10 +305,15 @@ var Dashboard = AbstractAction.extend(ControlPanelMixin, {
this.$searchview = $(QWeb.render("website.DateRangeButtons", {
widget: this,
}));
- this.$searchview.click('button.js_date_range', function(ev) {
+ this.$searchview.find('button.js_date_range').click(function(ev) {
+ self.$searchview.find('button.js_date_range.active').removeClass('active');
+ $(ev.target).addClass('active');
self.on_date_range_button($(ev.target).data('date'));
- $(this).find('button.js_date_range.active').removeClass('active');
+ });
+ this.$searchview.find('button.js_website').click(function(ev) {
+ self.$searchview.find('button.js_website.active').removeClass('active');
$(ev.target).addClass('active');
+ self.on_website_button($(ev.target).data('website-id'));
});
}
this.update_control_panel({
diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js
index 29163838f7a..162bd42c07b 100644
--- a/addons/website/static/src/js/content/website_root.js
+++ b/addons/website/static/src/js/content/website_root.js
@@ -40,6 +40,7 @@ var WebsiteRoot = BodyManager.extend({
'click .js_publish_management .js_publish_btn': '_onPublishBtnClick',
'submit .js_website_submit_form': '_onWebsiteFormSubmit',
'click .js_disable_on_click': '_onDisableOnClick',
+ 'click .js_multi_website_switch': '_multiWebsiteSwitch',
}),
custom_events: _.extend({}, BodyManager.prototype.custom_events || {}, {
animation_start_demand: '_onAnimationStartDemand',
@@ -279,6 +280,23 @@ var WebsiteRoot = BodyManager.extend({
_onDisableOnClick: function (ev) {
$(ev.currentTarget).addClass('disabled');
},
+
+ /**
+ * Called when clicking on the multi-website switcher.
+ *
+ * @param {OdooEvent} ev
+ */
+ _multiWebsiteSwitch: function (ev) {
+ var website_id_to_switch_to = ev.target.getAttribute('website-id');
+ this._rpc({
+ route: '/website/set_session_website',
+ params: {
+ website_id: website_id_to_switch_to || false,
+ },
+ }).then(function () {
+ window.location.reload(true);
+ });
+ },
});
return {
diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js
index 66679b5d20d..df4a2d0fe0d 100644
--- a/addons/website/static/src/js/menu/content.js
+++ b/addons/website/static/src/js/menu/content.js
@@ -145,6 +145,16 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
});
}));
+ defs.push(this._rpc({model: 'res.users',
+ method: 'has_group',
+ args: ['website.group_multi_website'],
+ context: context})
+ .then(function (has_group) {
+ if (!has_group) {
+ self.$('#website_restriction').addClass('hidden');
+ }
+ }));
+
var datepickersOptions = {
minDate: moment({y: 1900}),
maxDate: moment().add(200, 'y'),
@@ -210,6 +220,7 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({
is_menu: this.$('#is_menu').prop('checked'),
is_homepage: this.$('#is_homepage').prop('checked'),
website_published: this.$('#is_published').prop('checked'),
+ restrict_to_current_website: this.$('#restrict_to_current_website').prop('checked'),
create_redirect: this.$('#create_redirect').prop('checked'),
redirect_type: this.$('#redirect_type').val(),
website_indexed: this.$('#is_indexed').prop('checked'),
@@ -954,7 +965,7 @@ function _deletePage(pageId, fromPageManagement) {
// Delete the page if the user confirmed
return self._rpc({
model: 'website.page',
- method: 'delete_page',
+ method: 'unlink',
args: [pageId],
context: context,
});
diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml
index a1a0d24edb1..6bdb10f24c6 100644
--- a/addons/website/static/src/xml/website.backend.xml
+++ b/addons/website/static/src/xml/website.backend.xml
@@ -70,6 +70,14 @@
<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;">
+ <t t-foreach="widget.websites" t-as="website">
+ <button t-attf-class="btn btn-sm btn-default js_website #{website.selected ? 'active' : ''}"
+ t-att-data-website-id="website.id">
+ <t t-esc="website.name"/>
+ </button>
+ </t>
+ </div>
</t>
<t t-name="website.SalesChartTooltip">
diff --git a/addons/website/static/src/xml/website.pageProperties.xml b/addons/website/static/src/xml/website.pageProperties.xml
index 7f3b4cbfc64..39786f3449b 100644
--- a/addons/website/static/src/xml/website.pageProperties.xml
+++ b/addons/website/static/src/xml/website.pageProperties.xml
@@ -127,8 +127,23 @@
</label>
</div>
</div>
- <div class="form-group row">
- <label class="col-form-label col-md-3" for="date_publish">Publishing Date</label>
+ <div class="form-group" id="website_restriction">
+ <!-- TODO hide with multi-website group -->
+ <label class="control-label col-md-3" for="restrict_to_current_website">
+ Restrict to current website
+ <i class="fa fa-question-circle-o" title="When enabled, this page is only accessible through the current website" role="img" aria-label="Info"></i>
+ </label>
+ <div class="col-md-2">
+ <a>
+ <label class="o_switch" for="restrict_to_current_website" >
+ <input type="checkbox" t-att-checked="widget.page.website_id ? true : undefined" id="restrict_to_current_website"/>
+ <span/>
+ </label>
+ </a>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-md-3" for="date_publish">Publishing Date</label>
<div class="col-md-9">
<div class='input-group date' id='date_publish_container'>
<input type='text' class="form-control" name="date_publish" id="date_publish" />
diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py
index a4415acbb7d..1c57eeab793 100644
--- a/addons/website/tests/__init__.py
+++ b/addons/website/tests/__init__.py
@@ -6,3 +6,5 @@ from . import test_qweb
from . import test_crawl
from . import test_ui
from . import test_views
+from . import test_menu
+from . import test_page
diff --git a/addons/website/tests/test_menu.py b/addons/website/tests/test_menu.py
new file mode 100644
index 00000000000..e01b020bef8
--- /dev/null
+++ b/addons/website/tests/test_menu.py
@@ -0,0 +1,110 @@
+# coding: utf-8
+from odoo.tests import common
+
+
+class TestMenu(common.TransactionCase):
+ def setUp(self):
+ super(TestMenu, self).setUp()
+ Menu = self.env['website.menu']
+
+ self.menu_root = Menu.create({
+ 'name': 'Root',
+ })
+
+ self.menu_child = Menu.create({
+ 'name': 'Child',
+ 'parent_id': self.menu_root.id,
+ })
+
+ self.menu_leaf1 = Menu.create({
+ 'name': 'Leaf 1',
+ 'parent_id': self.menu_child.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):
+ 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([]))
+
+ 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([]))
diff --git a/addons/website/tests/test_page.py b/addons/website/tests/test_page.py
new file mode 100644
index 00000000000..76bfa52e51d
--- /dev/null
+++ b/addons/website/tests/test_page.py
@@ -0,0 +1,136 @@
+# coding: utf-8
+from odoo.tests import common
+
+
+class TestPage(common.TransactionCase):
+ def setUp(self):
+ super(TestPage, self).setUp()
+ View = self.env['ir.ui.view']
+ Page = self.env['website.page']
+ Menu = self.env['website.menu']
+
+ self.base_view = View.create({
+ 'name': 'Base',
+ 'type': 'qweb',
+ 'arch': '<div>content</div>',
+ 'key': 'test.base_view',
+ })
+
+ self.extension_view = View.create({
+ 'name': 'Extension',
+ 'mode': 'extension',
+ 'inherit_id': self.base_view.id,
+ 'arch': '<div position="inside">, extended content</div>',
+ })
+
+ self.page_1 = Page.create({
+ 'view_id': self.base_view.id,
+ 'url': '/page_1',
+ })
+
+ self.page_1_menu = Menu.create({
+ 'name': 'Page 1 menu',
+ 'page_id': self.page_1.id,
+ })
+
+ def test_cow_page(self):
+ Menu = self.env['website.menu']
+ Page = self.env['website.page']
+ View = self.env['ir.ui.view']
+
+ # backend write, no COW
+ total_pages = Page.search_count([])
+ total_menus = Menu.search_count([])
+ total_views = View.search_count([])
+ self.page_1.write({'arch': '<div>modified base content</div>'})
+ self.assertEqual(total_pages, Page.search_count([]))
+ self.assertEqual(total_menus, Menu.search_count([]))
+ self.assertEqual(total_views, View.search_count([]))
+
+ # 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
+ self.assertEqual(total_pages + 1, Page.search_count([]))
+ self.assertEqual(total_menus + 1, 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']
+ Page = self.env['website.page']
+ View = self.env['ir.ui.view']
+
+ # nothing special should happen when editing through the backend
+ total_pages = Page.search_count([])
+ total_menus = Menu.search_count([])
+ total_views = View.search_count([])
+ self.extension_view.write({'arch': '<div>modified extension content</div>'})
+ self.assertEqual(self.extension_view.arch, '<div>modified extension content</div>')
+ self.assertEqual(total_pages, Page.search_count([]))
+ self.assertEqual(total_menus, Menu.search_count([]))
+ self.assertEqual(total_views, View.search_count([]))
+
+ # When editing through the frontend a website-specific copy
+ # for the extension view should be created. When rendering the
+ # original website.page on website 1 it will look differently
+ # due to this new extension view.
+ self.extension_view.with_context(website_id=1).write({'arch': '<div>website 1 content</div>'})
+ self.assertEqual(total_pages, Page.search_count([]))
+ self.assertEqual(total_menus, Menu.search_count([]))
+ self.assertEqual(total_views + 1, View.search_count([]))
+
+ 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']
+
+ # currently the view unlink of website.page can't handle views with inherited views
+ 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']
+
+ # currently the view unlink of website.page can't handle views with inherited views
+ self.extension_view.unlink()
+
+ self.page_1.with_context(website_id=1).unlink()
+
+ self.assertEqual(bool(self.base_view.exists()), False)
+ self.assertEqual(bool(self.page_1.exists()), False)
+ 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)
diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py
index 7293eae4c56..10c2f3bcbf8 100644
--- a/addons/website/tests/test_views.py
+++ b/addons/website/tests/test_views.py
@@ -258,3 +258,86 @@ class TestViewSaving(common.TransactionCase):
))
)
)
+
+ def test_cow_leaf(self):
+ View = self.env['ir.ui.view']
+
+ base_view = View.create({
+ 'name': 'Base',
+ 'type': 'qweb',
+ 'arch': '<div>base content</div>',
+ }).with_context(load_all_views=True)
+
+ inherit_view = View.create({
+ 'name': 'Extension',
+ 'mode': 'extension',
+ 'inherit_id': base_view.id,
+ 'arch': '<div position="replace"><div>extended content</div></div>',
+ })
+
+ # edit on backend, regular write
+ inherit_view.write({'arch': '<div position="replace"><div>modified content</div></div>'})
+ self.assertEqual(View.search_count([('name', '=', 'Base')]), 1)
+ self.assertEqual(View.search_count([('name', '=', 'Extension')]), 1)
+
+ arch = base_view.read_combined(['arch'])['arch']
+ self.assertEqual(arch, '<div>modified content</div>')
+
+ # edit on frontend, copy just the leaf
+ inherit_view.with_context(website_id=1).write({'arch': '<div position="replace"><div>website 1 content</div></div>'})
+ inherit_views = View.search([('name', '=', 'Extension')])
+ self.assertEqual(View.search_count([('name', '=', 'Base')]), 1)
+ self.assertEqual(len(inherit_views), 2)
+ self.assertEqual(len(inherit_views.filtered(lambda v: v.website_id.id == 1)), 1)
+
+ # read in backend should be unaffected
+ arch = base_view.read_combined(['arch'])['arch']
+ self.assertEqual(arch, '<div>modified content</div>')
+ # read on website should reflect change
+ arch = base_view.with_context(website_id=1).read_combined(['arch'])['arch']
+ self.assertEqual(arch, '<div>website 1 content</div>')
+
+ # website-specific inactive view should take preference over active generic one when viewing the website
+ # this is necessary to make customize_show=True templates work correctly
+ inherit_views.filtered(lambda v: v.website_id.id == 1).write({'active': False})
+ arch = base_view.with_context(website_id=1).read_combined(['arch'])['arch']
+ self.assertEqual(arch, '<div>base content</div>')
+
+ def test_cow_root(self):
+ View = self.env['ir.ui.view']
+
+ base_view = View.create({
+ 'name': 'Base',
+ 'type': 'qweb',
+ 'arch': '<div>content</div>',
+ })
+
+ View.create({
+ 'name': 'Extension',
+ 'mode': 'extension',
+ 'inherit_id': base_view.id,
+ 'arch': '<div position="inside">, extended content</div>',
+ })
+
+ # edit on backend, regular write
+ base_view.write({'arch': '<div>modified base content</div>'})
+ self.assertEqual(View.search_count([('name', '=', 'Base')]), 1)
+ self.assertEqual(View.search_count([('name', '=', 'Extension')]), 1)
+
+ # edit on frontend, copy the entire tree
+ base_view.with_context(website_id=1).write({'arch': '<div>website 1 content</div>'})
+
+ generic_base_view = View.search([('name', '=', 'Base'), ('website_id', '=', False)])
+ website_specific_base_view = View.search([('name', '=', 'Base'), ('website_id', '=', 1)])
+ self.assertEqual(len(generic_base_view), 1)
+ self.assertEqual(len(website_specific_base_view), 1)
+
+ inherit_views = View.search([('name', '=', 'Extension')])
+ self.assertEqual(len(inherit_views), 2)
+ self.assertEqual(len(inherit_views.filtered(lambda v: v.website_id.id == 1)), 1)
+
+ arch = generic_base_view.with_context(load_all_views=True).read_combined(['arch'])['arch']
+ self.assertEqual(arch, '<div>modified base content, extended content</div>')
+
+ arch = website_specific_base_view.with_context(load_all_views=True, website_id=1).read_combined(['arch'])['arch']
+ self.assertEqual(arch, '<div>website 1 content, extended content</div>')
diff --git a/addons/website/views/ir_attachment_views.xml b/addons/website/views/ir_attachment_views.xml
new file mode 100644
index 00000000000..c553c280d2f
--- /dev/null
+++ b/addons/website/views/ir_attachment_views.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_attachment_form_inherit_website" model="ir.ui.view">
+ <field name="name">ir.attachment.form.inherit.website</field>
+ <field name="model">ir.attachment</field>
+ <field name="inherit_id" ref="base.view_attachment_form"/>
+ <field name="arch" type="xml">
+ <field name="mimetype" position="after">
+ <field name="key" groups="base.group_no_one"/>
+ <field name="theme_id" groups="base.group_no_one"/>
+ </field>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/website/views/res_config_settings_views.xml b/addons/website/views/res_config_settings_views.xml
index 8a19354c6f2..d8b44e6c65f 100644
--- a/addons/website/views/res_config_settings_views.xml
+++ b/addons/website/views/res_config_settings_views.xml
@@ -10,12 +10,27 @@
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Website" string="Website" data-key="website" groups="website.group_website_designer">
- <field name="website_id" invisible="1"/>
+ <h2 groups="website.group_multi_website">Select the Website to Configure</h2>
+ <div class="row mt16 o_settings_container" id="website_selection_settings" groups="website.group_multi_website">
+ <div class="col-xs-12 col-md-6 o_setting_box" id="website">
+ <div class="o_setting_right_pane">
+ <label string="Website" for="website_id"/>
+ <div class="text-muted">
+ Settings on this page will apply to this website
+ </div>
+ <field name="website_id" options="{'no_open': True, 'no_create_edit': True}"/>
+ <div>
+ <button name="website_go_to" type="object" string="Go to website"/>
+ </div>
+ </div>
+ </div>
+ </div>
<h2>Website</h2>
<div class="row mt16 o_settings_container" id="webmaster_settings">
<div class="col-12 col-lg-6 o_setting_box" id="domain_setting">
<div class="o_setting_right_pane">
- <span class="o_form_label">Website Title</span>
+ <label for="website_name" string="Website Title"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="text-muted">
Name and favicon of your website
</div>
@@ -31,9 +46,40 @@
</div>
</div>
</div>
+ <div class="col-12 col-lg-6 o_setting_box" id="company_settings" groups="base.group_multi_company">
+ <div class="o_setting_right_pane">
+ <label string="Company" for="website_company_id"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
+ <div class="text-muted">
+ The company this website belongs to
+ </div>
+ <field name="website_company_id"/>
+ </div>
+ </div>
+ <div class="col-12 col-lg-6 o_setting_box" id="domain_settings" groups="website.group_multi_website">
+ <div class="o_setting_right_pane">
+ <label string="Domain" for="website_domain"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
+ <div class="text-muted">
+ Display this website when users visit this domain
+ </div>
+ <field name="website_domain"/>
+ </div>
+ </div>
+ <div class="col-12 col-lg-6 o_setting_box" id="website_geoip_settings" groups="website.group_multi_website">
+ <div class="o_setting_right_pane">
+ <label string="Country Groups" for="website_country_group_ids"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
+ <div class="text-muted">
+ Display this website for users from these country groups
+ </div>
+ <field name="website_country_group_ids" widget="many2many_tags"/>
+ </div>
+ </div>
<div class="col-12 col-lg-6 o_setting_box" id="languages_setting">
<div class="o_setting_right_pane">
<label for="language_ids"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="text-muted">
Languages available on your website
</div>
@@ -58,6 +104,7 @@
</div>
<div class="o_setting_right_pane">
<label for="has_google_analytics"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="text-muted">
Track visits in Google Analytics
</div>
@@ -142,6 +189,7 @@
</div>
<div class="o_setting_right_pane">
<label for="auth_signup_uninvited"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="text-muted">
Let your customers log in to see their documents
</div>
@@ -155,12 +203,49 @@
</div>
</div>
</div>
+ <div class="col-12 col-lg-6 o_setting_box">
+ <div class="o_setting_left_pane"/>
+ <div class="o_setting_right_pane">
+ <label string="Social Media" for="social_twitter"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
+ <div class="text-muted">
+ Add links to social media on your website
+ </div>
+ <div class="content-group">
+ <div class="row">
+ <label for="social_twitter" string="Twitter" class="col-md-3 o_light_label"/>
+ <field name="social_twitter"/>
+ </div>
+ <div class="row">
+ <label for="social_facebook" string="Facebook" class="col-md-3 o_light_label"/>
+ <field name="social_facebook"/>
+ </div>
+ <div class="row">
+ <label for="social_github" string="GitHub" class="col-md-3 o_light_label"/>
+ <field name="social_github"/>
+ </div>
+ <div class="row">
+ <label for="social_linkedin" string="LinkedIn" class="col-md-3 o_light_label"/>
+ <field name="social_linkedin"/>
+ </div>
+ <div class="row">
+ <label for="social_youtube" string="YouTube" class="col-md-3 o_light_label"/>
+ <field name="social_youtube"/>
+ </div>
+ <div class="row">
+ <label for="social_googleplus" string="Google Plus" class="col-md-3 o_light_label"/>
+ <field name="social_googleplus"/>
+ </div>
+ </div>
+ </div>
+ </div>
<div class="col-12 col-lg-6 o_setting_box" id="cdn_setting" title="A CDN helps you serve your website’s content with high availability and high performance to any visitor wherever they are located." groups="base.group_no_one">
<div class="o_setting_left_pane">
<field name="cdn_activated"/>
</div>
<div class="o_setting_right_pane">
<label for="cdn_activated" string="Content Delivery Network (CDN)"/>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="text-muted">
Use a CDN to optimize the availability of your website's content
</div>
@@ -192,6 +277,20 @@
</div>
</div>
</div>
+ <h2>Multi-Website</h2>
+ <div class="row mt16 o_settings_container" id="multi_website" name="multi_website">
+ <div class="col-xs-12 col-md-6 o_setting_box">
+ <div class="o_setting_left_pane">
+ <field name="group_multi_website"/>
+ </div>
+ <div class="o_setting_right_pane">
+ <label string="Multi-Website" for="group_multi_website"/>
+ <div class="text-muted">
+ Manage multiple websites
+ </div>
+ </div>
+ </div>
+ </div>
</div>
</xpath>
</field>
@@ -227,6 +326,14 @@
<menuitem id="menu_website_add_features" parent="website.menu_website_global_configuration"
sequence="20" groups="base.group_system" action="action_website_add_features"/>
+ <menuitem name="Websites"
+ id="menu_website_websites_list"
+ action="action_website_list"
+ parent="menu_website_global_configuration"
+ groups="website.group_multi_website"
+ sequence="10"
+ />
+
<menuitem name="Pages"
id="menu_website_pages_list"
action="action_website_pages_list"
diff --git a/addons/website/views/res_partner_views.xml b/addons/website/views/res_partner_views.xml
new file mode 100644
index 00000000000..51bc3590725
--- /dev/null
+++ b/addons/website/views/res_partner_views.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<odoo>
+ <record id="view_partner_form_inherit_website" model="ir.ui.view">
+ <field name="name">res.partner.form.website.inherit</field>
+ <field name="model">res.partner</field>
+ <field name="inherit_id" ref="base.view_partner_form"/>
+ <field name="arch" type="xml">
+ <field name="company_id" position="after" options="{'no_create': True}">
+ <field name="website_id" groups="website.group_multi_website"/>
+ </field>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml
index 7433259ae5f..7f0adea2f56 100644
--- a/addons/website/views/website_navbar_templates.xml
+++ b/addons/website/views/website_navbar_templates.xml
@@ -77,6 +77,20 @@
<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>
+ </a>
+ </li>
+ </ul>
+ </li>
<!-- Page Edition -->
<li class="o_new_content_menu" id="new-content-menu">
<a href="#"><span class="fa fa-plus"/>New</a>
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 535887fde4e..6ff8df377d5 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -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.child_id" t-as="submenu">
+ <t t-foreach="website.menu_id.get_children_for_current_website()" t-as="submenu">
<t t-call="website.submenu">
<t t-set="submenu_type" t-value="'nav'"/>
</t>
@@ -1126,4 +1126,22 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
</t>
</template>
+<template id="default_homepage">
+ <div id="wrap" class="oe_structure oe_empty">
+ <div class="item image_text oe_img_bg active">
+ <div class="container">
+ <div class="row content">
+ <div class="col-md-6 col-sm-12">
+ <h2>Homepage of <t t-esc="website.name"/></h2>
+ <h3>Click to customize this text</h3>
+ <p>
+ <a role="button" class="btn btn-success btn-large" href="/contactus">Contact us</a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
</odoo>
diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml
index c48c7842b06..39107ad58e7 100644
--- a/addons/website/views/website_views.xml
+++ b/addons/website/views/website_views.xml
@@ -14,29 +14,15 @@
<field name="model">website</field>
<field name="arch" type="xml">
<form string="Website Settings">
- <header>
- <button string="Apply" type="object" name="noop" class="oe_highlight"/>
- <button string="Cancel" type="object" name="noop" class="oe_link"/>
- </header>
<div name="domain">
<separator name="domain" string="Domain"/>
<group name="domain">
<field name="name"/>
<field name="domain"/>
+ <field name="country_group_ids" widget="many2many_tags"/>
<field name="google_analytics_key" placeholder="UA-XXXXXXXX-Y"/>
</group>
</div>
- <div name="social_media">
- <separator string="Social Media"/>
- <group name="social_media">
- <field name="social_twitter" placeholder="https://twitter.com/Odoo"/>
- <field name="social_facebook" placeholder="https://www.facebook.com/odoo"/>
- <field name="social_googleplus" placeholder="https://plus.google.com/+Odooapps"/>
- <field name="social_linkedin" placeholder="https://www.linkedin.com/company/odoo"/>
- <field name="social_youtube" placeholder="https://www.youtube.com/user/OpenERPonline"/>
- <field name="social_github" placeholder="https://youraccount.github.io"/>
- </group>
- </div>
<div name="other">
<separator string="Other Info"/>
<group name="other">
@@ -55,8 +41,12 @@
<field name="arch" type="xml">
<tree string="Websites">
<field name="name"/>
+ <field name="domain"/>
+ <field name="country_group_ids" widget="many2many_tags"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="default_lang_id"/>
+ <field name="installed_theme_id" groups="base.group_no_one"/>
+ <field name="theme_ids" groups="base.group_no_one" widget="many2many_tags"/>
</tree>
</field>
</record>
@@ -77,6 +67,15 @@
</field>
</record>
+ <record id="action_website_list" model="ir.actions.act_window">
+ <field name="name">Websites</field>
+ <field name="res_model">website</field>
+ <field name="view_type">form</field>
+ <field name="view_mode">tree,form</field>
+ <field name="view_id" ref="view_website_tree"/>
+ <field name="target">current</field>
+ </record>
+
<!-- website.redirect views -->
<record id="website_redirect_form_view" model="ir.ui.view">
<field name="name">website.redirect.form</field>
@@ -91,7 +90,7 @@
<field name="url_to"/>
</group>
<group>
- <field name="website_id"/>
+ <field name="website_id" groups="website.group_multi_website"/>
<field name="active"/>
</group>
</group>
@@ -108,7 +107,7 @@
<field name="type"/>
<field name="url_from"/>
<field name="url_to"/>
- <field name="website_id"/>
+ <field name="website_id" groups="website.group_multi_website"/>
<field name="active"/>
<field name="sequence" widget="handle" />
</tree>
@@ -137,6 +136,8 @@
<field name="name"/>
<field name="url"/>
<field name="view_id"/>
+ <field groups="base.group_no_one" name="is_published"/>
+ <field groups="website.group_multi_website" name="website_id"/>
</group>
<group>
<field name="website_indexed"/>
@@ -144,7 +145,6 @@
<field name="date_publish"/>
</group>
</group>
- <field groups="base.group_no_one" name="website_ids"/>
<label for="menu_ids" string="Related Menu Items"/>
<field name="menu_ids"/>
</sheet>
@@ -193,7 +193,7 @@
<group>
<field name="new_window"/>
<field name="sequence"/>
- <field attrs="{'invisible': [('parent_id', '!=', False)]}" name="website_id"/>
+ <field name="website_id" groups="website.group_multi_website"/>
</group>
<group>
<field name="parent_id"/>
@@ -219,7 +219,7 @@
<field name="arch" type="xml">
<tree string="Website menu" editable="bottom">
<field name="sequence" widget="handle"/>
- <field name="website_id" options="{'no_create': True}"/>
+ <field name="website_id" options="{'no_create': True}" groups="website.group_multi_website"/>
<field name="name"/>
<field name="url"/>
<field name="new_window"/>
@@ -263,7 +263,8 @@
<field name="inherit_id" ref="base.view_view_form"/>
<field name="arch" type="xml">
<field name="name" position="after">
- <field name="website_id" readonly="1"/>
+ <field name="website_id" groups="website.group_multi_website"/>
+ <field name="theme_id" groups="base.group_no_one"/>
<field name="key" readonly="1"/>
<field name="page_ids" invisible="1" />
<field name="first_page_id" attrs="{'invisible': [('page_ids', '=', [])]}" />
diff --git a/addons/website/wizard/base_language_install_views.xml b/addons/website/wizard/base_language_install_views.xml
index c1abcc576aa..df3062d2f77 100644
--- a/addons/website/wizard/base_language_install_views.xml
+++ b/addons/website/wizard/base_language_install_views.xml
@@ -7,7 +7,7 @@
<field name="inherit_id" ref="base.view_base_language_install"/>
<field name="arch" type="xml">
<group states="init" position="inside">
- <field name="website_ids" widget="many2many_checkboxes"/>
+ <field name="website_ids" widget="many2many_checkboxes" groups="website.group_multi_website"/>
</group>
</field>
</record>
diff --git a/addons/website_crm/views/res_config_settings_views.xml b/addons/website_crm/views/res_config_settings_views.xml
index ee99fe898d5..e8f9f023fd0 100644
--- a/addons/website_crm/views/res_config_settings_views.xml
+++ b/addons/website_crm/views/res_config_settings_views.xml
@@ -5,7 +5,7 @@
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
<field name="arch" type="xml">
- <div id="webmaster_settings" position="after">
+ <div id="multi_website" position="after">
<h2>Communication</h2>
<div class="row mt16 o_settings_container" id="communication_settings">
<div class="col-12 col-lg-6 o_setting_box" id="contact_form_install_setting" title="New messages are managed as leads or opportunities in your CRM app.">
diff --git a/addons/website_livechat/views/res_config_settings_views.xml b/addons/website_livechat/views/res_config_settings_views.xml
index db77ff3203c..bdd8aa9f701 100644
--- a/addons/website_livechat/views/res_config_settings_views.xml
+++ b/addons/website_livechat/views/res_config_settings_views.xml
@@ -10,6 +10,7 @@
<div class="col-12 col-lg-6 o_setting_box" id="live_chat_install_setting">
<div class="o_setting_right_pane">
<span class="o_form_label">Live Chat</span>
+ <span class="fa fa-lg fa-globe" title="Values set here are website-specific." groups="website.group_multi_website"/>
<div class="text-muted">
Live chat channel of your website
</div>
diff --git a/addons/website_sale/__init__.py b/addons/website_sale/__init__.py
index 88710707227..5e8764bb37f 100644
--- a/addons/website_sale/__init__.py
+++ b/addons/website_sale/__init__.py
@@ -4,3 +4,4 @@
from . import controllers
from . import models
from . import wizard
+from . import report
diff --git a/addons/website_sale/controllers/backend.py b/addons/website_sale/controllers/backend.py
index eb5b0d78d26..b1906ffc9c1 100644
--- a/addons/website_sale/controllers/backend.py
+++ b/addons/website_sale/controllers/backend.py
@@ -12,8 +12,9 @@ from odoo.http import request
class WebsiteSaleBackend(WebsiteBackend):
@http.route()
- def fetch_dashboard_data(self, date_from, date_to):
- results = super(WebsiteSaleBackend, self).fetch_dashboard_data(date_from, date_to)
+ def fetch_dashboard_data(self, website_id, date_from, date_to):
+ results = super(WebsiteSaleBackend, self).fetch_dashboard_data(website_id, date_from, date_to)
+ current_website = request.env['website'].browse(website_id) if website_id else request.env['website'].get_current_website()
sales_values = dict(
graph=[],
@@ -40,6 +41,7 @@ class WebsiteSaleBackend(WebsiteBackend):
# Product-based computation
report_product_lines = request.env['sale.report'].read_group(
domain=[
+ ('website_id', '=', current_website.id),
('team_id.team_type', '=', 'website'),
('state', 'in', ['sale', 'done']),
('confirmation_date', '>=', date_from),
@@ -57,6 +59,7 @@ class WebsiteSaleBackend(WebsiteBackend):
# Sale-based results computation
sale_order_domain = [
+ ('website_id', '=', current_website.id),
('team_id', 'in', request.env['crm.team'].search([('team_type', '=', 'website')]).ids),
('date_order', '>=', fields.Datetime.to_string(datetime_from)),
('date_order', '<=', fields.Datetime.to_string(datetime_to))]
@@ -70,6 +73,7 @@ class WebsiteSaleBackend(WebsiteBackend):
report_price_lines = request.env['sale.report'].read_group(
domain=[
+ ('website_id', '=', current_website.id),
('team_id.team_type', '=', 'website'),
('state', 'in', ['sale', 'done']),
('date', '>=', date_from),
@@ -110,6 +114,7 @@ class WebsiteSaleBackend(WebsiteBackend):
previous_sale_label = _('Previous Year')
sales_domain = [
+ ('website_id', '=', current_website.id),
('team_id.team_type', '=', 'website'),
('state', 'in', ['sale', 'done']),
('confirmation_date', '>=', date_from),
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index c5a89397e47..b6803fc71b5 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -245,7 +245,7 @@ class WebsiteSale(http.Controller):
if attrib_list:
post['attrib'] = attrib_list
- categs = request.env['product.public.category'].search([('parent_id', '=', False)])
+ categs = request.env['product.public.category'].search([('parent_id', '=', False), ('website_published', '=', True)])
Product = request.env['product.template']
parent_category_ids = []
@@ -578,6 +578,7 @@ class WebsiteSale(http.Controller):
new_values['customer'] = True
new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id
+ new_values['website_id'] = request.website.id
lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None
if lang:
diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml
index 54e04004916..d4236f87da3 100644
--- a/addons/website_sale/data/demo.xml
+++ b/addons/website_sale/data/demo.xml
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
+ <record model="website" id="website.website2">
+ <field name="salesteam_id" ref="sales_team.salesteam_website_sales"/>
+ </record>
<record id="product.product_attribute_2" model="product.attribute">
<field name="type">color</field>
diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py
index c6fa80cd587..79a2a615019 100644
--- a/addons/website_sale/models/product.py
+++ b/addons/website_sale/models/product.py
@@ -53,7 +53,7 @@ class ProductPricelist(models.Model):
class ProductPublicCategory(models.Model):
_name = "product.public.category"
- _inherit = ["website.seo.metadata"]
+ _inherit = ["website.seo.metadata", "website.published.mixin"]
_description = "Website Product Category"
_order = "sequence, name"
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index 5a9141485e9..a48436c1433 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -25,6 +25,9 @@ class SaleOrder(models.Model):
only_services = fields.Boolean(compute='_compute_cart_info', string='Only Services')
is_abandoned_cart = fields.Boolean('Abandoned Cart', compute='_compute_abandoned_cart', search='_search_abandoned_cart')
cart_recovery_email_sent = fields.Boolean('Cart recovery email already sent')
+ website_id = fields.Many2one('website', related='partner_id.website_id', string='Website',
+ help='Website through which this order was placed.',
+ store=True, readonly=True)
@api.multi
@api.depends('website_order_line.product_uom_qty', 'website_order_line.product_id')
diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py
index dbb1756a5a9..6990d3a76de 100644
--- a/addons/website_sale/models/website.py
+++ b/addons/website_sale/models/website.py
@@ -172,6 +172,7 @@ class Website(models.Model):
'partner_invoice_id': partner.id,
'partner_shipping_id': addr['delivery'],
'user_id': salesperson_id or self.salesperson_id.id or default_user_id,
+ 'website_id': self._context.get('website_id'),
}
company = self.company_id or pricelist.company_id
if company:
diff --git a/addons/website_sale/report/__init__.py b/addons/website_sale/report/__init__.py
new file mode 100644
index 00000000000..29ad993b414
--- /dev/null
+++ b/addons/website_sale/report/__init__.py
@@ -0,0 +1,2 @@
+# coding: utf-8
+from . import sale_report
diff --git a/addons/website_sale/report/sale_report.py b/addons/website_sale/report/sale_report.py
new file mode 100644
index 00000000000..9427fc320fe
--- /dev/null
+++ b/addons/website_sale/report/sale_report.py
@@ -0,0 +1,21 @@
+# coding: utf-8
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo import api, fields, models
+
+
+class SaleReport(models.Model):
+ _inherit = 'sale.report'
+
+ 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
diff --git a/addons/website_sale/views/product_views.xml b/addons/website_sale/views/product_views.xml
index 706460b6454..219eaab5909 100644
--- a/addons/website_sale/views/product_views.xml
+++ b/addons/website_sale/views/product_views.xml
@@ -172,6 +172,11 @@
<field name="arch" type="xml">
<form string="Website Public Categories">
<sheet>
+ <div class="oe_button_box" name="button_box">
+ <button class="oe_stat_button" name="website_publish_button" type="object" icon="fa-globe">
+ <field name="website_published" widget="website_button"/>
+ </button>
+ </div>
<field name="image_medium" widget='image' class="oe_avatar"/>
<div class="oe_left">
<group>
@@ -219,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" />
+ <field name="website_id" string="Allow to use on " placeholder="None website" groups="website.group_multi_website"/>
</field>
</field>
</record>
diff --git a/addons/website_sale/views/sale_order_views.xml b/addons/website_sale/views/sale_order_views.xml
index e80895b0678..0bad33449e3 100644
--- a/addons/website_sale/views/sale_order_views.xml
+++ b/addons/website_sale/views/sale_order_views.xml
@@ -228,4 +228,14 @@
action="product.variants_action"
parent="menu_product_settings" groups="product.group_product_variant" sequence="3"/>
+ <record id="sale_order_view_form" model="ir.ui.view">
+ <field name="name">sale.order.form</field>
+ <field name="model">sale.order</field>
+ <field name="inherit_id" ref="sale.view_order_form"/>
+ <field name="arch" type="xml">
+ <field name="origin" position="after">
+ <field name="website_id" groups="website.group_multi_website"/>
+ </field>
+ </field>
+ </record>
</odoo>
diff --git a/addons/website_sale_link_tracker/controllers/backend.py b/addons/website_sale_link_tracker/controllers/backend.py
index d80261238b1..f301d1c4a2b 100644
--- a/addons/website_sale_link_tracker/controllers/backend.py
+++ b/addons/website_sale_link_tracker/controllers/backend.py
@@ -9,8 +9,8 @@ from odoo.http import request
class WebsiteSaleLinkTrackerBackend(WebsiteSaleBackend):
@http.route()
- def fetch_dashboard_data(self, date_from, date_to):
- results = super(WebsiteSaleLinkTrackerBackend, self).fetch_dashboard_data(date_from, date_to)
+ def fetch_dashboard_data(self, website_id, date_from, date_to):
+ results = super(WebsiteSaleLinkTrackerBackend, self).fetch_dashboard_data(website_id, date_from, date_to)
results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(date_from, date_to)
return results
diff --git a/addons/website_sale_stock/__manifest__.py b/addons/website_sale_stock/__manifest__.py
index 9b3f75e93fb..6eb8f199bec 100644
--- a/addons/website_sale_stock/__manifest__.py
+++ b/addons/website_sale_stock/__manifest__.py
@@ -13,7 +13,8 @@
'data': [
'views/product_template_views.xml',
'views/res_config_settings_views.xml',
- 'views/website_sale_stock_templates.xml'
+ 'views/website_sale_stock_templates.xml',
+ 'views/stock_picking_views.xml'
],
'demo': [
'data/website_sale_stock_demo.xml',
diff --git a/addons/website_sale_stock/models/__init__.py b/addons/website_sale_stock/models/__init__.py
index 65614897665..ee736eceac4 100644
--- a/addons/website_sale_stock/models/__init__.py
+++ b/addons/website_sale_stock/models/__init__.py
@@ -6,3 +6,4 @@ from . import product_product
from . import product_template
from . import res_config_settings
from . import sale_order
+from . import stock_picking
diff --git a/addons/website_sale_stock/models/stock_picking.py b/addons/website_sale_stock/models/stock_picking.py
new file mode 100644
index 00000000000..7a3fdf29eef
--- /dev/null
+++ b/addons/website_sale_stock/models/stock_picking.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models, api, fields
+from odoo.tools.translate import _
+
+
+class StockPicking(models.Model):
+ _inherit = 'stock.picking'
+
+ website_id = fields.Many2one('website', related='partner_id.website_id', string='Website',
+ help='Website this picking belongs to.',
+ store=True, readonly=True)
+
diff --git a/addons/website_sale_stock/views/stock_picking_views.xml b/addons/website_sale_stock/views/stock_picking_views.xml
new file mode 100644
index 00000000000..5b5d5ba7502
--- /dev/null
+++ b/addons/website_sale_stock/views/stock_picking_views.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="view_picking_form_inherit_website_sale_stock" model="ir.ui.view">
+ <field name="name">stock.picking.form.inherit.website.sale.stock</field>
+ <field name="model">stock.picking</field>
+ <field name="inherit_id" ref="stock.view_picking_form"/>
+ <field name="arch" type="xml">
+ <field name="company_id" position="before">
+ <field name="website_id" groups="website.group_multi_website"/>
+ </field>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/website_theme_install/__manifest__.py b/addons/website_theme_install/__manifest__.py
index 1fca59f40c5..854673b0b29 100644
--- a/addons/website_theme_install/__manifest__.py
+++ b/addons/website_theme_install/__manifest__.py
@@ -7,6 +7,7 @@
'data': [
'views/assets.xml',
'views/views.xml',
+ 'views/res_config_settings_views.xml',
],
'depends': ['website'],
'auto_install': True,
diff --git a/addons/website_theme_install/models/__init__.py b/addons/website_theme_install/models/__init__.py
index de58fda6e31..786d2febfb6 100644
--- a/addons/website_theme_install/models/__init__.py
+++ b/addons/website_theme_install/models/__init__.py
@@ -2,3 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import ir_module_module
+from . import ir_ui_view
+from . import res_config_settings
diff --git a/addons/website_theme_install/models/ir_module_module.py b/addons/website_theme_install/models/ir_module_module.py
index 5350abea437..560bd37a932 100644
--- a/addons/website_theme_install/models/ir_module_module.py
+++ b/addons/website_theme_install/models/ir_module_module.py
@@ -2,9 +2,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import os
+import logging
from odoo import api, fields, models
+_logger = logging.getLogger(__name__)
+
class IrModuleModule(models.Model):
_name = "ir.module.module"
@@ -13,6 +16,71 @@ class IrModuleModule(models.Model):
image_ids = fields.One2many('ir.attachment', 'res_id',
domain=[('res_model', '=', _name), ('mimetype', '=like', 'image/%')],
string='Screenshots', readonly=True)
+ is_installed_on_current_website = fields.Boolean(compute='_compute_is_installed_on_current_website')
+
+ @api.multi
+ def _compute_is_installed_on_current_website(self):
+ for module in self:
+ module.is_installed_on_current_website = module == self.env['website'].get_current_website().installed_theme_id
+
+ @api.multi
+ def write(self, vals):
+ if vals.get('state') == 'installed':
+ self._handle_auto_installs()
+
+ return super(IrModuleModule, self).write(vals)
+
+ @api.multi
+ def _assign_views(self):
+ IrModelData = self.env['ir.model.data']
+ for module in self:
+ views = self.env['ir.ui.view'].browse(IrModelData.search([('module', '=', module.name),
+ ('model', '=', 'ir.ui.view')]).mapped('res_id'))
+ views.write({
+ 'theme_id': module.id
+ })
+
+ @api.model
+ def _handle_auto_installs(self):
+ for module in self.search([('auto_install', '=', True), ('name', '=like', 'theme%'), ('state', 'in', ('installed', 'to install'))]):
+ theme_deps = module.get_upstream_theme_dependencies()
+ websites = self.env['website'].search([]).filtered(lambda w: len(theme_deps - w.theme_ids) == 0)
+ if websites:
+ _logger.info('auto-loading %s on %s', module.name, websites.mapped('name'))
+ websites.write({'theme_ids': [(4, module.id, 0)]})
+ module._assign_views()
+
+ # put demo date on first website
+ module._make_demo_data_website_specific(websites[0])
+
+ @api.multi
+ def _get_module_data(self, model_name):
+ IrModelData = self.env['ir.model.data']
+ records = self.env[model_name]
+
+ for module in self:
+ records |= self.env[model_name].browse(IrModelData.search([('module', '=', module.name), ('model', '=', model_name)]).mapped('res_id'))
+
+ return records
+
+ @api.multi
+ def _make_demo_data_website_specific(self, website):
+ for module in self:
+ menus = module._get_module_data('website.menu')
+ menus.with_context(no_cow=True).write({'website_id': website.id})
+
+ pages = module._get_module_data('website.page')
+ pages.with_context(no_cow=True).write({'website_id': website.id})
+ pages.with_context(no_cow=True).mapped('view_id').write({'website_id': website.id})
+
+ @api.multi
+ def _remove_website_specific_demo_data(self, website):
+ for module in self:
+ menus = module._get_module_data('website.menu')
+ menus.filtered(lambda menu: menu.website_id == website).unlink()
+
+ pages = module._get_module_data('website.page')
+ pages.filtered(lambda page: page.website_id == website).unlink()
@api.model
def update_list(self):
@@ -40,6 +108,11 @@ class IrModuleModule(models.Model):
return res
+ def get_upstream_theme_dependencies(self):
+ upstream_deps = self.upstream_dependencies(exclude_states=('uninstallable', 'to remove'))
+ upstream_theme_deps = upstream_deps.filtered(lambda mod: any(mod.name.startswith(keyword) for keyword in ('theme_', 'snippet_', 'website_animate')))
+ return upstream_theme_deps
+
@api.multi
def button_choose_theme(self):
theme_category = self.env.ref('base.module_category_theme', False)
@@ -47,15 +120,71 @@ class IrModuleModule(models.Model):
theme_category_id = theme_category.id if theme_category else 0
hidden_categories_ids = [theme_hidden_category.id if theme_hidden_category else 0]
+ current_website = self.env['website'].get_current_website()
- self.search([ # Uninstall the theme(s) which is (are) installed
+ themes_to_uninstall = self.search([ # Uninstall the theme(s) which is (are) installed
('state', '=', 'installed'),
('category_id', 'not in', hidden_categories_ids),
+ ('id', 'in', current_website.theme_ids.ids),
'|', ('category_id', '=', theme_category_id), ('category_id.parent_id', '=', theme_category_id)
- ]).button_immediate_uninstall()
+ ])
+
+ if themes_to_uninstall:
+ themes_to_uninstall.button_immediate_uninstall()
+
+ modules_belonging_to_theme = self + self.get_upstream_theme_dependencies()
+ current_website.installed_theme_id = self
+ current_website.theme_ids |= modules_belonging_to_theme
next_action = self.button_immediate_install() # Then install the new chosen one
+ modules_belonging_to_theme._assign_views()
+ modules_belonging_to_theme._make_demo_data_website_specific(current_website)
+ self._handle_auto_installs()
+ _logger.info('installed themes %s on %s', modules_belonging_to_theme.mapped('name'), current_website.name)
+
if next_action.get('tag') == 'reload' and not next_action.get('params', {}).get('menu_id'):
next_action = self.env.ref('website.action_website').read()[0]
return next_action
+
+ @api.multi
+ def button_uninstall(self):
+ if not any(mod.name.startswith('theme_') for mod in self):
+ return super(IrModuleModule, self).button_uninstall()
+
+ # only one theme can be uninstalled
+ self.ensure_one()
+
+ Website = self.env['website']
+ current_website = Website.get_current_website()
+ if Website.search_count([('theme_ids', 'in', self.id)]) > 1:
+ modules_belonging_to_theme = self + self.get_upstream_theme_dependencies() + self.downstream_dependencies()
+ current_website.installed_theme_id -= modules_belonging_to_theme
+ current_website.theme_ids -= modules_belonging_to_theme
+ modules_belonging_to_theme._remove_website_specific_demo_data(current_website)
+ _logger.info('removed themes %s from %s', modules_belonging_to_theme.mapped('name'), current_website.name)
+ return True
+ else:
+ uninstalled_modules = self + self.downstream_dependencies()
+ res = super(IrModuleModule, self).button_uninstall()
+
+ for website in Website.search([]):
+ website.installed_theme_id -= uninstalled_modules
+ website.theme_ids -= uninstalled_modules
+ _logger.info('removed themes %s from %s', uninstalled_modules.mapped('name'), website.name)
+
+ # Attempt to uninstall upstream dependencies. This is
+ # necessary for uninstalling e.g. theme_zap, when doing so
+ # theme_treehouse should also be removed from the current
+ # website. This way the user can install a completely new
+ # theme.
+ #
+ # Make sure to only uninstall every module once with
+ # context. Otherwise it might get removed from the current
+ # website the first time and the second time it might end
+ # up being wrongly uninstalled.
+ if self._context.get('uninstall_upstream') is not False:
+ for upstream_dependency in self.get_upstream_theme_dependencies().filtered(lambda mod: mod.name.startswith('theme_')):
+ upstream_dependency.with_context(uninstall_upstream=False).button_uninstall()
+
+ return res
diff --git a/addons/website_theme_install/models/ir_ui_view.py b/addons/website_theme_install/models/ir_ui_view.py
new file mode 100644
index 00000000000..a222fbd884f
--- /dev/null
+++ b/addons/website_theme_install/models/ir_ui_view.py
@@ -0,0 +1,35 @@
+# coding: utf-8
+from odoo import api, fields, models
+
+class View(models.Model):
+ _inherit = 'ir.ui.view'
+
+ @api.model
+ def _get_inheriting_views_arch_website(self, view_id):
+ res = self.env['website'].get_current_website()
+
+ # In certain cases view validation as done by
+ # _validate_module_views will fail. When attempting to load
+ # inherited view CHILD:
+ #
+ # PARENT (theme_common)
+ # ^
+ # |
+ # CHILD (other_theme)
+ #
+ # During the view validation no website_id will be in context
+ # so the view will fail to apply, since PARENT won't be
+ # selected by the inheriting view arch domain.
+ #
+ # This is not a problem however, CHILD will never be loaded on
+ # a website where CHILD is not installed. To simulate this,
+ # return a website which has the theme containing CHILD
+ # installed.
+ if res:
+ return res
+ else:
+ view_theme = self.browse(view_id).theme_id
+ if view_theme:
+ return self.env['website'].search([('theme_ids', 'in', view_theme.id)], limit=1)
+ else:
+ return self.env['website']
diff --git a/addons/website_theme_install/models/res_config_settings.py b/addons/website_theme_install/models/res_config_settings.py
new file mode 100644
index 00000000000..307ecc932f8
--- /dev/null
+++ b/addons/website_theme_install/models/res_config_settings.py
@@ -0,0 +1,14 @@
+# coding: utf-8
+from odoo import api, fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = 'res.config.settings'
+
+ def website_install_theme(self):
+ self.website_id._fix_to_session()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/web#action=website_theme_install.theme_install_kanban_action',
+ 'target': 'self',
+ }
diff --git a/addons/website_theme_install/views/res_config_settings_views.xml b/addons/website_theme_install/views/res_config_settings_views.xml
new file mode 100644
index 00000000000..635b202b187
--- /dev/null
+++ b/addons/website_theme_install/views/res_config_settings_views.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="res_config_settings_view_form" model="ir.ui.view">
+ <field name="name">res.config.settings.view.form.inherit.website_theme_install</field>
+ <field name="model">res.config.settings</field>
+ <field name="inherit_id" ref="website.res_config_settings_view_form"/>
+ <field name="arch" type="xml">
+ <button name="website_go_to" position="after">
+ <button name="website_install_theme" type="object" string="Change theme"/>
+ </button>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/website_theme_install/views/views.xml b/addons/website_theme_install/views/views.xml
index 37271fa817d..a915dc47f18 100644
--- a/addons/website_theme_install/views/views.xml
+++ b/addons/website_theme_install/views/views.xml
@@ -13,17 +13,21 @@
<field name="image_ids"/>
<field name="category_id"/>
<field name="display_name"/>
+ <field name="is_installed_on_current_website"/>
<templates>
- <div t-name="kanban-box" t-attf-class="o_theme_preview mb16 mt16 #{record.state.raw_value == 'installed'? 'o_theme_installed' : ''}">
+ <div t-name="kanban-box" t-attf-class="o_theme_preview mb16 mt16 #{record.is_installed_on_current_website.raw_value? 'o_theme_installed' : ''}">
<t t-set="has_image" t-value="record.image_ids.raw_value.length > 0"/>
<t t-set="has_screenshot" t-value="record.image_ids.raw_value.length > 1"/>
<t t-set="image_url" t-value="has_image ? 'web/image/' + record.image_ids.raw_value[0] : record.icon.value"/>
<div class="o_theme_preview_top bg-white mb4">
<div class="o_browser_bar"/>
- <i states="installed" class="text-center bg-white text-success rounded-circle fa fa-check-circle" role="img" aria-label="Installed" title="Installed"/>
+ <i states="installed" t-if="record.is_installed_on_current_website.raw_value" class="text-center bg-white text-success img-circle fa fa-check-circle" role="img" aria-label="Installed" title="Installed"/>
<div t-attf-class="bg-gray-lighter #{has_screenshot? 'o_theme_screenshot' : (has_image ? 'o_theme_cover' : 'o_theme_logo')}" t-attf-style="background-image: url(#{image_url});"/>
- <div states="uninstalled" class="o_button_area">
+ <div t-if="record.is_installed_on_current_website.raw_value" class="o_button_area">
+ <button type="object" name="button_immediate_uninstall" class="btn btn-primary">Uninstall theme</button>
+ </div>
+ <div t-else="" class="o_button_area">
<button type="object" name="button_choose_theme" class="btn btn-primary">Use this theme</button>
<hr t-if="record.url.value"/>
<a role="button" t-if="record.url.value" class="btn btn-secondary" t-att-href="record.url.value" target="_blank">Live Preview</a>
@@ -59,9 +63,14 @@
</field>
</record>
+ <!-- themes should be installed through website_theme_install -->
+ <record id="base.open_module_tree" model="ir.actions.act_window">
+ <field name="domain">['!', ('name', '=like', 'theme_%')]</field>
+ </record>
+
<!-- Actions to list themes with custom kanban (launched on module installation) -->
<record id="theme_install_kanban_action" model="ir.actions.act_window">
- <field name="name">Choose Your Theme</field>
+ <field name="name">Choose a theme for this website</field>
<field name="res_model">ir.module.module</field>
<field name="view_mode">kanban</field>
<field name="view_id" ref="theme_view_kanban"/>
@@ -71,6 +80,7 @@
'|', ('category_id', '=', ref('base.module_category_theme', False)), ('category_id.parent_id', '=', ref('base.module_category_theme', False))
]"/>
</record>
+
<record id="theme_install_todo_action" model="ir.actions.server">
<field name="name">Config: Choose Your Theme</field>
<field name="model_id" ref="model_ir_module_module"/>