Initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from . import color_editor
|
||||
from . import ir_http
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
from . import res_users_settings
|
||||
133
Fusion Backend Theme/fusion_backend_theme/models/color_editor.py
Normal file
133
Fusion Backend Theme/fusion_backend_theme/models/color_editor.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import re
|
||||
import base64
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.tools import misc
|
||||
|
||||
from odoo.addons.base.models.assetsbundle import EXTENSIONS
|
||||
|
||||
|
||||
class ColorEditor(models.AbstractModel):
|
||||
|
||||
_name = 'fusion_backend_theme.color_editor'
|
||||
_description = 'Theme Color Editor'
|
||||
|
||||
@api.model
|
||||
def _get_custom_url(self, url, bundle):
|
||||
return f'/_custom/{bundle}{url}'
|
||||
|
||||
@api.model
|
||||
def _get_url_info(self, url):
|
||||
regex = re.compile(
|
||||
r'^(/_custom/([^/]+))?/(\w+)/([/\w.]+\.\w+)$'
|
||||
)
|
||||
match = regex.match(url)
|
||||
if not match:
|
||||
return False
|
||||
return {
|
||||
'module': match.group(3),
|
||||
'resource_path': match.group(4),
|
||||
'customized': bool(match.group(1)),
|
||||
'bundle': match.group(2) or False
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _find_attachment(self, custom_url):
|
||||
return self.env['ir.attachment'].search([
|
||||
('url', '=', custom_url)
|
||||
])
|
||||
|
||||
@api.model
|
||||
def _find_asset(self, custom_url):
|
||||
return self.env['ir.asset'].search([
|
||||
('path', 'like', custom_url)
|
||||
])
|
||||
|
||||
@api.model
|
||||
def _read_scss(self, url, bundle):
|
||||
custom_url = self._get_custom_url(url, bundle)
|
||||
url_info = self._get_url_info(custom_url)
|
||||
if url_info and url_info['customized']:
|
||||
attachment = self._find_attachment(custom_url)
|
||||
if attachment:
|
||||
return base64.b64decode(attachment.datas)
|
||||
with misc.file_open(
|
||||
url.strip('/'), 'rb', filter_ext=EXTENSIONS
|
||||
) as f:
|
||||
return f.read()
|
||||
|
||||
def _extract_variable(self, content, variable):
|
||||
value = re.search(
|
||||
fr'\$o_fusion_{variable}\:?\s(.*?);', content
|
||||
)
|
||||
return value and value.group(1)
|
||||
|
||||
def _extract_variables(self, content, variables):
|
||||
return {
|
||||
var: self._extract_variable(content, var)
|
||||
for var in variables
|
||||
}
|
||||
|
||||
def _update_variables(self, content, variables):
|
||||
for variable in variables:
|
||||
if not variable.get("value"):
|
||||
continue
|
||||
content = re.sub(
|
||||
fr'{variable["name"]}\:?\s(.*?);',
|
||||
f'{variable["name"]}: {variable["value"]};',
|
||||
content
|
||||
)
|
||||
return content
|
||||
|
||||
@api.model
|
||||
def _write_scss(self, url, bundle, content):
|
||||
custom_url = self._get_custom_url(url, bundle)
|
||||
asset_url = url[1:] if url.startswith(('/', '\\')) else url
|
||||
datas = base64.b64encode((content or '\n').encode('utf-8'))
|
||||
custom_attachment = self._find_attachment(custom_url)
|
||||
if custom_attachment:
|
||||
custom_attachment.write({'datas': datas})
|
||||
self.env.registry.clear_cache('assets')
|
||||
else:
|
||||
attachment_values = {
|
||||
'name': url.split('/')[-1],
|
||||
'type': 'binary',
|
||||
'mimetype': 'text/scss',
|
||||
'datas': datas,
|
||||
'url': custom_url,
|
||||
}
|
||||
asset_values = {
|
||||
'path': custom_url,
|
||||
'target': url,
|
||||
'directive': 'replace',
|
||||
}
|
||||
target_asset = self._find_asset(asset_url)
|
||||
if target_asset:
|
||||
asset_values['name'] = '%s override' % target_asset.name
|
||||
asset_values['bundle'] = target_asset.bundle
|
||||
asset_values['sequence'] = target_asset.sequence
|
||||
else:
|
||||
asset_values['name'] = '%s: replace %s' % (
|
||||
bundle, custom_url.split('/')[-1]
|
||||
)
|
||||
asset_values['bundle'] = (
|
||||
self.env['ir.asset']._get_related_bundle(url, bundle)
|
||||
)
|
||||
self.env['ir.attachment'].create(attachment_values)
|
||||
self.env['ir.asset'].create(asset_values)
|
||||
|
||||
def get_variable_values(self, url, bundle, variables):
|
||||
content = self._read_scss(url, bundle)
|
||||
return self._extract_variables(
|
||||
content.decode('utf-8'), variables
|
||||
)
|
||||
|
||||
def set_variable_values(self, url, bundle, variables):
|
||||
original = self._read_scss(url, bundle).decode('utf-8')
|
||||
content = self._update_variables(original, variables)
|
||||
self._write_scss(url, bundle, content)
|
||||
|
||||
def reset_asset(self, url, bundle):
|
||||
custom_url = self._get_custom_url(url, bundle)
|
||||
self._find_attachment(custom_url).unlink()
|
||||
self._find_asset(custom_url).unlink()
|
||||
42
Fusion Backend Theme/fusion_backend_theme/models/ir_http.py
Normal file
42
Fusion Backend Theme/fusion_backend_theme/models/ir_http.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from odoo import models
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
|
||||
_inherit = "ir.http"
|
||||
|
||||
@classmethod
|
||||
def _post_logout(cls):
|
||||
super()._post_logout()
|
||||
request.future_response.set_cookie('color_scheme', max_age=0)
|
||||
|
||||
def color_scheme(self):
|
||||
cookie_scheme = request.httprequest.cookies.get('color_scheme')
|
||||
scheme = cookie_scheme if cookie_scheme else super().color_scheme()
|
||||
if user := request.env.user:
|
||||
if user._is_public():
|
||||
return super().color_scheme()
|
||||
if user_scheme := user.res_users_settings_id.color_scheme:
|
||||
if user_scheme in ('light', 'dark'):
|
||||
return user_scheme
|
||||
return scheme
|
||||
|
||||
def session_info(self):
|
||||
result = super().session_info()
|
||||
user = self.env.user
|
||||
if user._is_internal():
|
||||
for company in user.company_ids.with_context(bin_size=True):
|
||||
result['user_companies']['allowed_companies'][company.id].update({
|
||||
'has_background_image': bool(company.background_image),
|
||||
'has_appsbar_image': bool(company.appbar_image),
|
||||
})
|
||||
result['chatter_position'] = user.chatter_position
|
||||
result['dialog_size'] = user.dialog_size
|
||||
result['pager_autoload_interval'] = int(
|
||||
self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_backend_theme.pager_autoload_interval',
|
||||
default=30000
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,19 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
|
||||
_inherit = 'res.company'
|
||||
|
||||
favicon = fields.Binary(
|
||||
string="Company Favicon",
|
||||
attachment=True,
|
||||
)
|
||||
background_image = fields.Binary(
|
||||
string='Apps Menu Background Image',
|
||||
attachment=True,
|
||||
)
|
||||
appbar_image = fields.Binary(
|
||||
string='Sidebar Logo Image',
|
||||
attachment=True,
|
||||
)
|
||||
@@ -0,0 +1,264 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Properties
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def COLOR_FIELDS(self):
|
||||
return [
|
||||
'color_brand',
|
||||
'color_primary',
|
||||
'color_success',
|
||||
'color_info',
|
||||
'color_warning',
|
||||
'color_danger',
|
||||
]
|
||||
|
||||
@property
|
||||
def THEME_COLOR_FIELDS(self):
|
||||
return [
|
||||
'color_appsmenu_text',
|
||||
'color_sidebar_text',
|
||||
'color_sidebar_active',
|
||||
'color_sidebar_background',
|
||||
]
|
||||
|
||||
@property
|
||||
def COLOR_ASSET_LIGHT_URL(self):
|
||||
return '/fusion_backend_theme/static/src/scss/colors_light.scss'
|
||||
|
||||
@property
|
||||
def COLOR_BUNDLE_LIGHT_NAME(self):
|
||||
return 'web._assets_primary_variables'
|
||||
|
||||
@property
|
||||
def COLOR_ASSET_DARK_URL(self):
|
||||
return '/fusion_backend_theme/static/src/scss/colors_dark.scss'
|
||||
|
||||
@property
|
||||
def COLOR_BUNDLE_DARK_NAME(self):
|
||||
return 'web.assets_web_dark'
|
||||
|
||||
@property
|
||||
def COLOR_ASSET_THEME_URL(self):
|
||||
return '/fusion_backend_theme/static/src/scss/colors_light.scss'
|
||||
|
||||
@property
|
||||
def COLOR_BUNDLE_THEME_NAME(self):
|
||||
return 'web._assets_primary_variables'
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Company Assets
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
theme_favicon = fields.Binary(
|
||||
related='company_id.favicon',
|
||||
readonly=False,
|
||||
)
|
||||
theme_background_image = fields.Binary(
|
||||
related='company_id.background_image',
|
||||
readonly=False,
|
||||
)
|
||||
appbar_image = fields.Binary(
|
||||
related='company_id.appbar_image',
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Light Mode Colors
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
color_brand_light = fields.Char(string='Brand Light Color')
|
||||
color_primary_light = fields.Char(string='Primary Light Color')
|
||||
color_success_light = fields.Char(string='Success Light Color')
|
||||
color_info_light = fields.Char(string='Info Light Color')
|
||||
color_warning_light = fields.Char(string='Warning Light Color')
|
||||
color_danger_light = fields.Char(string='Danger Light Color')
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Dark Mode Colors
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
color_brand_dark = fields.Char(string='Brand Dark Color')
|
||||
color_primary_dark = fields.Char(string='Primary Dark Color')
|
||||
color_success_dark = fields.Char(string='Success Dark Color')
|
||||
color_info_dark = fields.Char(string='Info Dark Color')
|
||||
color_warning_dark = fields.Char(string='Warning Dark Color')
|
||||
color_danger_dark = fields.Char(string='Danger Dark Color')
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Theme Colors
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
theme_color_appsmenu_text = fields.Char(string='App Switcher Text Color')
|
||||
theme_color_sidebar_text = fields.Char(string='Sidebar Text Color')
|
||||
theme_color_sidebar_active = fields.Char(string='Sidebar Active Color')
|
||||
theme_color_sidebar_background = fields.Char(string='Sidebar Background Color')
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Light Color Helpers
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def _get_light_color_values(self):
|
||||
return self.env['fusion_backend_theme.color_editor'].get_variable_values(
|
||||
self.COLOR_ASSET_LIGHT_URL,
|
||||
self.COLOR_BUNDLE_LIGHT_NAME,
|
||||
self.COLOR_FIELDS,
|
||||
)
|
||||
|
||||
def _set_light_color_values(self, values):
|
||||
colors = self._get_light_color_values()
|
||||
for var, value in colors.items():
|
||||
values[f'{var}_light'] = value
|
||||
return values
|
||||
|
||||
def _detect_light_color_change(self):
|
||||
colors = self._get_light_color_values()
|
||||
return any(
|
||||
val is not None and self[f'{var}_light'] != val
|
||||
for var, val in colors.items()
|
||||
)
|
||||
|
||||
def _replace_light_color_values(self):
|
||||
variables = [
|
||||
{'name': field, 'value': self[f'{field}_light']}
|
||||
for field in self.COLOR_FIELDS
|
||||
]
|
||||
return self.env['fusion_backend_theme.color_editor'].set_variable_values(
|
||||
self.COLOR_ASSET_LIGHT_URL,
|
||||
self.COLOR_BUNDLE_LIGHT_NAME,
|
||||
variables,
|
||||
)
|
||||
|
||||
def _reset_light_color_assets(self):
|
||||
self.env['fusion_backend_theme.color_editor'].reset_asset(
|
||||
self.COLOR_ASSET_LIGHT_URL,
|
||||
self.COLOR_BUNDLE_LIGHT_NAME,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Dark Color Helpers
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def _get_dark_color_values(self):
|
||||
return self.env['fusion_backend_theme.color_editor'].get_variable_values(
|
||||
self.COLOR_ASSET_DARK_URL,
|
||||
self.COLOR_BUNDLE_DARK_NAME,
|
||||
self.COLOR_FIELDS,
|
||||
)
|
||||
|
||||
def _set_dark_color_values(self, values):
|
||||
colors = self._get_dark_color_values()
|
||||
for var, value in colors.items():
|
||||
values[f'{var}_dark'] = value
|
||||
return values
|
||||
|
||||
def _detect_dark_color_change(self):
|
||||
colors = self._get_dark_color_values()
|
||||
return any(
|
||||
val is not None and self[f'{var}_dark'] != val
|
||||
for var, val in colors.items()
|
||||
)
|
||||
|
||||
def _replace_dark_color_values(self):
|
||||
variables = [
|
||||
{'name': field, 'value': self[f'{field}_dark']}
|
||||
for field in self.COLOR_FIELDS
|
||||
]
|
||||
return self.env['fusion_backend_theme.color_editor'].set_variable_values(
|
||||
self.COLOR_ASSET_DARK_URL,
|
||||
self.COLOR_BUNDLE_DARK_NAME,
|
||||
variables,
|
||||
)
|
||||
|
||||
def _reset_dark_color_assets(self):
|
||||
self.env['fusion_backend_theme.color_editor'].reset_asset(
|
||||
self.COLOR_ASSET_DARK_URL,
|
||||
self.COLOR_BUNDLE_DARK_NAME,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Theme Color Helpers
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def _get_theme_color_values(self):
|
||||
return self.env['fusion_backend_theme.color_editor'].get_variable_values(
|
||||
self.COLOR_ASSET_THEME_URL,
|
||||
self.COLOR_BUNDLE_THEME_NAME,
|
||||
self.THEME_COLOR_FIELDS,
|
||||
)
|
||||
|
||||
def _set_theme_color_values(self, values):
|
||||
colors = self._get_theme_color_values()
|
||||
for var, value in colors.items():
|
||||
values[f'theme_{var}'] = value
|
||||
return values
|
||||
|
||||
def _detect_theme_color_change(self):
|
||||
colors = self._get_theme_color_values()
|
||||
return any(
|
||||
val is not None and self[f'theme_{var}'] != val
|
||||
for var, val in colors.items()
|
||||
)
|
||||
|
||||
def _replace_theme_color_values(self):
|
||||
variables = [
|
||||
{'name': field, 'value': self[f'theme_{field}']}
|
||||
for field in self.THEME_COLOR_FIELDS
|
||||
]
|
||||
return self.env['fusion_backend_theme.color_editor'].set_variable_values(
|
||||
self.COLOR_ASSET_THEME_URL,
|
||||
self.COLOR_BUNDLE_THEME_NAME,
|
||||
variables,
|
||||
)
|
||||
|
||||
def _reset_theme_color_assets(self):
|
||||
self.env['fusion_backend_theme.color_editor'].reset_asset(
|
||||
self.COLOR_ASSET_THEME_URL,
|
||||
self.COLOR_BUNDLE_THEME_NAME,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Actions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def action_reset_light_color_assets(self):
|
||||
self._reset_light_color_assets()
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
def action_reset_dark_color_assets(self):
|
||||
self._reset_dark_color_assets()
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
def action_reset_theme_color_assets(self):
|
||||
self._reset_light_color_assets()
|
||||
self._reset_dark_color_assets()
|
||||
self._reset_theme_color_assets()
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# CRUD
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def get_values(self):
|
||||
res = super().get_values()
|
||||
res = self._set_light_color_values(res)
|
||||
res = self._set_dark_color_values(res)
|
||||
res = self._set_theme_color_values(res)
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
res = super().set_values()
|
||||
if self._detect_light_color_change():
|
||||
self._replace_light_color_values()
|
||||
if self._detect_dark_color_change():
|
||||
self._replace_dark_color_values()
|
||||
if self._detect_theme_color_change():
|
||||
self._replace_theme_color_values()
|
||||
return res
|
||||
@@ -0,0 +1,57 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
|
||||
_inherit = "res.users"
|
||||
|
||||
color_scheme = fields.Selection(
|
||||
related="res_users_settings_id.color_scheme",
|
||||
readonly=False,
|
||||
)
|
||||
sidebar_type = fields.Selection(
|
||||
selection=[
|
||||
('invisible', 'Invisible'),
|
||||
('small', 'Small'),
|
||||
('large', 'Large'),
|
||||
],
|
||||
string="Sidebar Type",
|
||||
default='large',
|
||||
required=True,
|
||||
)
|
||||
chatter_position = fields.Selection(
|
||||
selection=[
|
||||
('side', 'Side'),
|
||||
('bottom', 'Bottom'),
|
||||
],
|
||||
string="Chatter Position",
|
||||
default='side',
|
||||
required=True,
|
||||
)
|
||||
dialog_size = fields.Selection(
|
||||
selection=[
|
||||
('minimize', 'Minimize'),
|
||||
('maximize', 'Maximize'),
|
||||
],
|
||||
string="Dialog Size",
|
||||
default='minimize',
|
||||
required=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + [
|
||||
'color_scheme',
|
||||
'sidebar_type',
|
||||
'chatter_position',
|
||||
'dialog_size',
|
||||
]
|
||||
|
||||
@property
|
||||
def SELF_WRITEABLE_FIELDS(self):
|
||||
return super().SELF_WRITEABLE_FIELDS + [
|
||||
'color_scheme',
|
||||
'sidebar_type',
|
||||
'chatter_position',
|
||||
'dialog_size',
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
|
||||
_inherit = 'res.users.settings'
|
||||
|
||||
homemenu_config = fields.Json(
|
||||
string="Home Menu Configuration",
|
||||
readonly=True,
|
||||
)
|
||||
color_scheme = fields.Selection(
|
||||
[("system", "System"), ("light", "Light"), ("dark", "Dark")],
|
||||
default="system",
|
||||
required=True,
|
||||
string="Color Scheme",
|
||||
)
|
||||
Reference in New Issue
Block a user