@ -0,0 +1,40 @@ |
|||||
|
Code Backend Theme |
||||
|
================== |
||||
|
* Code Backend Theme module for Odoo 15 community editions |
||||
|
|
||||
|
Installation |
||||
|
============ |
||||
|
- www.odoo.com/documentation/15.0/setup/install.html |
||||
|
- Install our custom addon |
||||
|
|
||||
|
License |
||||
|
------- |
||||
|
General Public License, Version 3 (LGPL v3). |
||||
|
(https://www.odoo.com/documentation/user/14.0/legal/licenses/licenses.html) |
||||
|
|
||||
|
Company |
||||
|
------- |
||||
|
* 'Cybrosys Techno Solutions <https://cybrosys.com/>'__ |
||||
|
|
||||
|
Credits |
||||
|
------- |
||||
|
* 'Cybrosys Techno Solutions <https://cybrosys.com/>'__ |
||||
|
|
||||
|
Contacts |
||||
|
-------- |
||||
|
* Mail Contact : odoo@cybrosys.com |
||||
|
|
||||
|
Bug Tracker |
||||
|
----------- |
||||
|
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. |
||||
|
|
||||
|
Maintainer |
||||
|
========== |
||||
|
This module is maintained by Cybrosys Technologies. |
||||
|
|
||||
|
For support and more information, please visit https://www.cybrosys.com |
||||
|
|
||||
|
Further information |
||||
|
=================== |
||||
|
HTML Description: `<static/description/index.html>`__ |
||||
|
|
@ -0,0 +1,22 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
||||
|
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU LESSER |
||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
||||
|
# (LGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################# |
||||
|
from .hooks import test_pre_init_hook, test_post_init_hook |
@ -0,0 +1,71 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
||||
|
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU LESSER |
||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
||||
|
# (LGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################# |
||||
|
|
||||
|
{ |
||||
|
"name": "Code Backend Theme V15", |
||||
|
"description": """Minimalist and elegant backend theme for Odoo 14, Backend Theme, Theme""", |
||||
|
"summary": "Code Backend Theme V15 is an attractive theme for backend", |
||||
|
"category": "Theme/Backend", |
||||
|
"version": "15.0.1.0.0", |
||||
|
'author': 'Cybrosys Techno Solutions', |
||||
|
'company': 'Cybrosys Techno Solutions', |
||||
|
'maintainer': 'Cybrosys Techno Solutions', |
||||
|
'website': "https://www.cybrosys.com", |
||||
|
"depends": ['base', 'web', 'mail'], |
||||
|
"data": [ |
||||
|
'views/icons.xml', |
||||
|
'views/layout.xml', |
||||
|
], |
||||
|
'assets': { |
||||
|
'web.assets_frontend': [ |
||||
|
'code_backend_theme/static/src/scss/login.scss', |
||||
|
], |
||||
|
'web.assets_backend': [ |
||||
|
'code_backend_theme/static/src/scss/theme_accent.scss', |
||||
|
'code_backend_theme/static/src/scss/navigation_bar.scss', |
||||
|
'code_backend_theme/static/src/scss/datetimepicker.scss', |
||||
|
'code_backend_theme/static/src/scss/theme.scss', |
||||
|
'code_backend_theme/static/src/scss/sidebar.scss', |
||||
|
('replace', '/web/static/src/views/graph/colors.js', '/code_backend_theme/static/src/js/fields/colors.js'), |
||||
|
('replace', '/web/static/src/views/graph/graph_renderer.js', '/code_backend_theme/static/src/js/fields/graph_renderer.js'), |
||||
|
('replace', '/web/static/src/views/graph/graph_model.js', '/code_backend_theme/static/src/js/fields/graph_model.js'), |
||||
|
('replace', '/web/static/src/views/graph/graph_arch_parser.js', '/code_backend_theme/static/src/js/fields/graph_arch_parser.js'), |
||||
|
('replace', '/web/static/src/views/graph/graph_view.js', '/code_backend_theme/static/src/js/fields/graph_view.js'), |
||||
|
'code_backend_theme/static/src/js/chrome/sidebar_menu.js', |
||||
|
('replace', '/web/static/src/webclient/user_menu/user_menu.js', '/code_backend_theme/static/src/js/user_menu/user_menu.js'), |
||||
|
], |
||||
|
'web.assets_qweb': [ |
||||
|
'code_backend_theme/static/src/xml/styles.xml', |
||||
|
'code_backend_theme/static/src/xml/top_bar.xml', |
||||
|
], |
||||
|
}, |
||||
|
'images': [ |
||||
|
'static/description/banner.png', |
||||
|
'static/description/theme_screenshot.png', |
||||
|
], |
||||
|
'license': 'LGPL-3', |
||||
|
'pre_init_hook': 'test_pre_init_hook', |
||||
|
'post_init_hook': 'test_post_init_hook', |
||||
|
'installable': True, |
||||
|
'application': False, |
||||
|
'auto_install': False, |
||||
|
} |
@ -0,0 +1,7 @@ |
|||||
|
## Module <code_backend_theme> |
||||
|
|
||||
|
#### 22.10.2021 |
||||
|
#### Version 15.0.1.0.0 |
||||
|
#### ADD |
||||
|
Initial Commit |
||||
|
|
@ -0,0 +1,292 @@ |
|||||
|
"""Hooks for Changing Menu Web_icon""" |
||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################# |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
||||
|
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU LESSER |
||||
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
||||
|
# (LGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################# |
||||
|
import base64 |
||||
|
|
||||
|
from odoo import api, SUPERUSER_ID |
||||
|
from odoo.modules import get_module_resource |
||||
|
|
||||
|
|
||||
|
def test_pre_init_hook(cr): |
||||
|
"""pre init hook""" |
||||
|
|
||||
|
env = api.Environment(cr, SUPERUSER_ID, {}) |
||||
|
menu_item = env['ir.ui.menu'].search([('parent_id', '=', False)]) |
||||
|
|
||||
|
for menu in menu_item: |
||||
|
if menu.name == 'Contacts': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Contacts.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Link Tracker': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Link Tracker.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Dashboards': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Dashboards.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Sales': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Sales.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Invoicing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Invoicing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Inventory': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Inventory.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Purchase': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Purchase.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Calendar': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Calendar.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'CRM': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'CRM.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Note': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Note.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Website': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Website.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Point of Sale': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Point of Sale.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Manufacturing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Manufacturing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Repairs': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Repairs.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Email Marketing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Email Marketing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'SMS Marketing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'SMS Marketing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Project': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Project.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Surveys': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Surveys.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Employees': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Employees.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Recruitment': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Recruitment.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Attendances': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Attendances.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Time Off': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Time Off.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Expenses': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Expenses.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Maintenance': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Maintenance.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Live Chat': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Live Chat.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Lunch': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Lunch.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Fleet': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Fleet.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Timesheets': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Timesheets.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Events': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Events.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'eLearning': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'eLearning.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Members': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Members.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
|
||||
|
|
||||
|
def test_post_init_hook(cr, registry): |
||||
|
"""post init hook""" |
||||
|
|
||||
|
env = api.Environment(cr, SUPERUSER_ID, {}) |
||||
|
menu_item = env['ir.ui.menu'].search([('parent_id', '=', False)]) |
||||
|
|
||||
|
for menu in menu_item: |
||||
|
if menu.name == 'Contacts': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Contacts.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Link Tracker': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Link Tracker.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Dashboards': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Dashboards.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Sales': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Sales.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Invoicing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Invoicing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Inventory': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Inventory.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Purchase': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Purchase.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Calendar': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Calendar.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'CRM': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'CRM.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Note': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Note.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Website': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Website.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Point of Sale': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Point of Sale.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Manufacturing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Manufacturing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Repairs': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Repairs.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Email Marketing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Email Marketing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'SMS Marketing': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'SMS Marketing.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Project': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Project.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Surveys': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Surveys.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Employees': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Employees.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Recruitment': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Recruitment.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Attendances': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Attendances.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Time Off': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Time Off.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Expenses': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Expenses.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Maintenance': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Maintenance.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Live Chat': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Live Chat.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Lunch': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Lunch.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Fleet': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Fleet.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Timesheets': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Timesheets.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Events': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Events.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'eLearning': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'eLearning.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
||||
|
if menu.name == 'Members': |
||||
|
img_path = get_module_resource( |
||||
|
'code_backend_theme', 'static', 'src', 'img', 'icons', 'Members.png') |
||||
|
menu.write({'web_icon_data': base64.b64encode(open(img_path, "rb").read())}) |
After Width: | Height: | Size: 120 KiB |
After Width: | Height: | Size: 168 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 905 B |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 722 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 614 KiB |
After Width: | Height: | Size: 369 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 272 KiB |
After Width: | Height: | Size: 128 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 313 KiB |
After Width: | Height: | Size: 178 KiB |
After Width: | Height: | Size: 116 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 114 KiB |
After Width: | Height: | Size: 344 KiB |
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 194 KiB |
After Width: | Height: | Size: 232 KiB |
After Width: | Height: | Size: 27 KiB |
@ -0,0 +1,956 @@ |
|||||
|
<!-- HERO --> |
||||
|
<div class="container"> |
||||
|
<div class="row" style="padding: 4rem 2.5rem 0 !important; background-color: #fff !important;"> |
||||
|
<div class="col-lg-12 d-flex flex-column align-items-center"> |
||||
|
<h1 class="text-center text-uppercase" |
||||
|
style="font-family: Montserrat, 'sans-serif' !important; font-weight: bolder !important; font-size: 2.5rem !important; color: #212121;"> |
||||
|
Code Backend Theme <sup> |
||||
|
</h1> |
||||
|
<p class="my-1 text-center text-uppercase" |
||||
|
style="letter-spacing: 4px !important; color: #74788D !important;">Minimalist and Elegant Backend |
||||
|
Theme for Odoo 15</p> |
||||
|
</div> |
||||
|
<div class="col-lg-12 d-flex justify-content-center align-items-center" style="margin: 2rem 0;"> |
||||
|
<img src="./assets/hero.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF HERO --> |
||||
|
|
||||
|
<!-- OVERVIEW --> |
||||
|
<div class="container"> |
||||
|
<div class="row" style="padding: 0rem 2.5rem !important; background-color: #fff !important;"> |
||||
|
<div class="col-lg-12 d-flex flex-column justify-content-center"> |
||||
|
<p class="my-1 text-center" |
||||
|
style="font-family: Montserrat, 'sans-serif' !important; color: #212121 !important;"> |
||||
|
The Code Backend Theme V15 Gives You a Fully Modified View with a Full Screen Display. |
||||
|
This is a Minimalist and Elegant Backend Theme for Odoo 15. |
||||
|
This Theme Will Change Your Old Experience to a New Experience With Odoo. |
||||
|
It is a Perfect Choice for Your Odoo Backend and an Attractive Theme for Your Odoo 15. |
||||
|
It will Give You a Clean Layout with a New Color Combination and a Modified Font. It has a |
||||
|
Sidebar with |
||||
|
New App Icons and Company Logo. This Will Change Your Old Kanban, List and Form Views to A Fully |
||||
|
Modified View. |
||||
|
</p> |
||||
|
</div> |
||||
|
<div class="col-lg-12 mt-4"> |
||||
|
<div class="alert alert-warning text-center" role="alert"> |
||||
|
<i class="fa fa-exclamation-triangle mr-2" aria-hidden="true"></i>Please make sure that you install |
||||
|
all |
||||
|
your apps prior to the installation of this theme. |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF OVERVIEW--> |
||||
|
|
||||
|
|
||||
|
<!-- FEATURE --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
||||
|
|
||||
|
<h2 |
||||
|
style="font-weight: 300 !important; background-color: #fff !important; z-index: 1 !important; padding: 0 1rem !important;"> |
||||
|
Features</h2> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- RESPONSIVE --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #556EE6 !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/responsive.jpg" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg-4 d-flex justify-content-center align-items-center" |
||||
|
style="padding: 1.5rem !important; margin: 0rem 0rem 3rem !important;"> |
||||
|
<img src="assets/resp-gif.gif" width="80%" height="auto" class="img-responsive rounded"> |
||||
|
</div> |
||||
|
<div class="col-lg-8" style="padding: 2.5rem 1.5rem!important;"> |
||||
|
<div class="text-center" |
||||
|
style="font-size: 0.9rem !important; background-color: #556EE6 !important; padding: 0.5 1.5rem !important; width: 60px; color: #ffffff !important; font-weight: 700; border-radius: 0.2rem !important; margin: 10px 0 !important;"> |
||||
|
New |
||||
|
</div> |
||||
|
<h3 style="font-weight: 700 !important;">Fully Responsive Layout</h3> |
||||
|
<h6 |
||||
|
style="font-style: Montserrat, 'sans-serif' !important; color: #2A3042 !important; font-weight: 300 !important;"> |
||||
|
Now take advantage of everything your dashboard has to offer even on the go. Our design are |
||||
|
now |
||||
|
fully responsive enabling you to view and manage everything from the comfort of your mobile |
||||
|
device. Everything |
||||
|
has been designed in a meticulous fashion so that every view snaps itself to fit the size of |
||||
|
the |
||||
|
device you are using, be it smartphones, tablet or any other portables, our theme adjusts |
||||
|
itself |
||||
|
to fit the screen size. |
||||
|
</h6> |
||||
|
<span class="d-flex" style="margin-top: 2rem !important;"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Fully responsive</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Fly-out hamburger menu on the left</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Fits perfectly to all screen sizes</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Quick access menu at the bottom in discuss</p> |
||||
|
</span> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF RESPONSIVE --> |
||||
|
<!-- FEATURE --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #556EE6 !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2.5rem 1.5rem!important;"> |
||||
|
<h3 style="font-weight: 700 !important;">Kanban Group View</h3> |
||||
|
<h6 |
||||
|
style="font-style: Montserrat, 'sans-serif' !important; color: #2A3042 !important; font-weight: 300 !important;"> |
||||
|
The Code Backend Theme V15 Gives You a Fully Modified Kanban View and Kanban Group View. |
||||
|
The Section Wise Separated Stages give a Pleasant Experience And an Extraordinary Design |
||||
|
To Your Content Tiles Making The Tiles Look Great. |
||||
|
It will Give You a Clean Layout with the New Color Combination and a Modified Font. |
||||
|
</h6> |
||||
|
<div class="row mt-4"> |
||||
|
<div class="col-lg-6"> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Modified Font</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Color Combination</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Full Screen View</p> |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-6"> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Stages are Separated in View</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Clean Layout</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Buttons with New Colors</p> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/kanabangroupview.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF FEATURE --> |
||||
|
|
||||
|
<!-- FEATURE --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #556EE6 !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2.5rem 1.5rem!important;"> |
||||
|
<h3 style="font-weight: 700 !important;">List View</h3> |
||||
|
<h6 |
||||
|
style="font-style: Montserrat, 'sans-serif' !important; color: #2A3042 !important; font-weight: 300 !important;"> |
||||
|
The All new Code Backend Theme V15 Gives You The Fully Modified List View and This Table Design |
||||
|
is Also Have Awesome Design and it Gives You More Beauty for Your Odoo Backend. |
||||
|
It will Give You a Clean Layout with the New Color Combination and a Modified Font. |
||||
|
</h6> |
||||
|
<div class="row mt-4"> |
||||
|
<div class="col-lg-6"> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Modified Table Style</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Color Combination</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Scroll Bar</p> |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-6"> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Status Tag</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Scrollbar</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Buttons with New Colors</p> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/listview.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF FEATURE --> |
||||
|
|
||||
|
<!-- FEATURE --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #556EE6 !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2.5rem 1.5rem!important;"> |
||||
|
<h3 style="font-weight: 700 !important;">Form View</h3> |
||||
|
<h6 |
||||
|
style="font-style: Montserrat, 'sans-serif' !important; color: #2A3042 !important; font-weight: 300 !important;"> |
||||
|
Code Backend Theme Gives You The Fully Modified Form View with a Full Screen Experience. It will |
||||
|
Give You a Clean Layout with the New Color Combination |
||||
|
and a Modified Font. |
||||
|
</h6> |
||||
|
<div class="row mt-4"> |
||||
|
<div class="col-lg-6"> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Modified Form Style</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Full Screen Form View</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Looks for Tabs</p> |
||||
|
</span> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-6"> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Style for Required Field</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Chatter Style Under Form View</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Looks for Status Button</p> |
||||
|
</span> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/Form view.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF FEATURE --> |
||||
|
|
||||
|
<!-- TWO COLUMN BLOCK --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row" style="margin: 2rem; ; min-width: 100% !important;"> |
||||
|
|
||||
|
<div class="col-lg-8" style="padding: 1rem 1rem 1rem 0rem !important;"> |
||||
|
<div class=" shadow" |
||||
|
style="background-color: #fff !important; border-top: 3px solid #556EE6 !important; padding: 2.5rem 0rem 0rem 0rem !important;"> |
||||
|
<h3 class="mx-4 mt-3" style="font-weight: 700 !important;">Overview</h3> |
||||
|
<h6 class="mx-4" |
||||
|
style="font-style: Montserrat, 'sans-serif' !important; color: #2A3042 !important; font-weight: 300 !important;"> |
||||
|
Code Backend Theme V15 is an Attractive Theme for Your Odoo 15. |
||||
|
This Theme Will Change Improve Your Experience With Odoo. |
||||
|
This is a Minimalist and Elegant Backend Theme for Odoo 15 And Can Offer a Perfect Choice |
||||
|
for |
||||
|
Your Odoo Backend. |
||||
|
</h6> |
||||
|
<div class="mx-4 my-4"> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Modified Structure for All Type Views</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Style for Active Menus, Radio Buttons and Checkboxes</p> |
||||
|
</span> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Color Combination</p> |
||||
|
</span> |
||||
|
|
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>New Look for All Applications</p> |
||||
|
</span> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>A Clean layout and New Font Style</p> |
||||
|
</span> |
||||
|
<span class="d-flex"> |
||||
|
<i class="fa fa-check-square mr-2" |
||||
|
style="color:#556EE6 !important; margin-top: 5px !important;"></i> |
||||
|
<p>Sidebar with New Menu Icons</p> |
||||
|
</span> |
||||
|
|
||||
|
</div> |
||||
|
<img src="./assets/all_screens.png" class="img-responsive" width="100% !important" |
||||
|
height="auto !important"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4" style="padding: 1rem 0rem 1rem 1rem!important;"> |
||||
|
<div class="shadow" |
||||
|
style="background-color: #fff !important; border-top: 3px solid #556EE6 !important; padding: 2.5rem 0rem 0rem 1.5rem !important; position: relative; overflow: hidden !important;"> |
||||
|
<div class="text-center" |
||||
|
style="font-size: 0.9rem !important; background-color: #556EE6 !important; padding: 0.5 1.5rem !important; width: 60px; color: #ffffff !important; font-weight: 700; border-radius: 0.2rem !important; margin: 10px 0 !important;"> |
||||
|
New |
||||
|
</div> |
||||
|
<h3 class="mt-3" style="font-weight: 700 !important;">All-New Menu Design</h3> |
||||
|
|
||||
|
<h6 |
||||
|
style="font-style: Montserrat, 'sans-serif' !important; color: #2A3042 !important; font-weight: 300 !important; padding-bottom: 3.8rem !important;"> |
||||
|
The All-New Menu Design is Main Attractive Section for the Code Backend Theme. The Sidebar |
||||
|
have New Minimalist |
||||
|
Icons for Applications in Odoo. Also the Sidebar Have Closing and Opening Option. |
||||
|
Customisable Logo Attached in Sidebar |
||||
|
That is Automatically Fetch Your Company Logo. |
||||
|
</h6> |
||||
|
<img src="./assets/menu_focus.png" class="img-responsive" width="100% !important" |
||||
|
height="auto !important"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF TWO COLUMN BLOCK --> |
||||
|
|
||||
|
|
||||
|
<!-- FEATURE --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #556EE6 !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2.5rem 1.5rem!important;"> |
||||
|
<h3 style="font-weight: 700 !important;">Easily Access Sidebar Menu</h3> |
||||
|
<h6 |
||||
|
style="font-style: Montserrat, 'sans-serif' !important; color: #2A3042 !important; font-weight: 300 !important;"> |
||||
|
Reveal the sidebar menu with just a click. Sidebar menu features all the relevant links to |
||||
|
navigate |
||||
|
through the application. |
||||
|
Hiding the sidebar leaves more space on the main area offering a distraction-free view that lets |
||||
|
you |
||||
|
focus on what matters the most. |
||||
|
</h6> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/easily-access-menu.gif" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- END OF FEATURE --> |
||||
|
|
||||
|
<!-- SCREENSHOTS --> |
||||
|
<div class="container" style="margin-top: 3rem;"> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
||||
|
|
||||
|
<h2 |
||||
|
style="font-weight: 300 !important; background-color: #fff !important; z-index: 1 !important; padding: 0 1rem !important;"> |
||||
|
Screenshots</h2> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">1</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Login Page</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/login.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">2</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Group By View</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/2.groupbyview.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">3</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Settings Page</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/3.settings page.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">4</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Discuss Page</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/4.discusspage.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">5</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Product Kanban View</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/5.productskanaban.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">6</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Purchase List View</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/6.purchase view.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">7</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Product View with Smart Buttons</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/7.productviewsmartbuttons.png" width="100%" height="auto" |
||||
|
class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">8</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Modified Alert Notifications are Placed on the Right Bottom of Display |
||||
|
</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/8error.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">9</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Wizards and User Error Popups</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/modal.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">10</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">New Looks for The Tabs</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/10.newlookoftabs.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">11</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Recruitment Kanban View With Ribbons</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/11.recruitment.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">12</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Sales Kanban View</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/12.saleskanban.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">13</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Modified Kanban View for Employees With New Designed Category Section</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/13.modified kanban employee.png" width="100%" height="auto" |
||||
|
class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">14</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Sidebar with List View</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/15.sidebarwithlistview.png" width="100%" height="auto" |
||||
|
class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
|
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">15</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Attendance Pages</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/17.attendanceview.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row shadow" |
||||
|
style="margin: 2rem; padding: 0rem !important; background-color: #fff !important; border-top: 3px solid #74788D !important; min-width: 100% !important;"> |
||||
|
<div class="col-lg-12" style="padding: 2rem 1.5rem!important;"> |
||||
|
<div class="d-flex my-3"> |
||||
|
<div class="d-flex justify-content-center align-items-center" |
||||
|
style="background-color: #556EE6 !important; border: 4px solid #D4DAF9 !important; color: #fff !important; height: 35px; width: 35px; border-radius: 50% !important; font-size: 1.1rem !important;"> |
||||
|
<h6 style="margin-top: 0.3rem; color: #fff !important;">16</h6> |
||||
|
</div> |
||||
|
<h6 class="mt-2 ml-2">Graphs with Sidebar</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-12" style="padding-left: 0 !important; padding-right: 0!important;"> |
||||
|
<img src="assets/screenshots/16grapghview.png" width="100%" height="auto" class="img-responsive"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- END OF SCREENSHOTS --> |
||||
|
|
||||
|
<!-- OUR SERVICES --> |
||||
|
<section class="container" style="margin-top: 6rem !important;"> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mb-4"> |
||||
|
|
||||
|
<h2 |
||||
|
style="font-weight: 300 !important; background-color: #fff !important; z-index: 1 !important; padding: 0 1rem !important;"> |
||||
|
Our Services</h2> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #1dd1a1 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/cogs.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Customization</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #ff6b6b !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/wrench.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Implementation</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #6462CD !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/lifebuoy.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Support</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #ffa801 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/user.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Hire |
||||
|
Odoo |
||||
|
Developer</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #54a0ff !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/puzzle.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Integration</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #6d7680 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/update.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Migration</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #786fa6 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/consultation.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Consultancy</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #f8a5c2 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/training.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Implementation</h6> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
||||
|
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
||||
|
style="background-color: #e6be26 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
||||
|
<img src="assets/icons/license.png" class="img-responsive" height="48px" width="48px"> |
||||
|
</div> |
||||
|
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
||||
|
Odoo |
||||
|
Licensing Consultancy</h6> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
<!-- END OF END OF OUR SERVICES --> |
||||
|
|
||||
|
<!-- OUR INDUSTRIES --> |
||||
|
<section class="container" style="margin-top: 6rem !important; background-color: #fff !important;"> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mb-4"> |
||||
|
|
||||
|
<h2 |
||||
|
style="font-weight: 300 !important; background-color: #fff !important; z-index: 1 !important; padding: 0 1rem !important;"> |
||||
|
Our Industries</h2> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/trading-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Trading |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">Easily |
||||
|
procure |
||||
|
and |
||||
|
sell your products</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/pos-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
POS |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">Easy |
||||
|
configuration |
||||
|
and convivial experience</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/education-black.png" class="img-responsive mb-3" height="48px" |
||||
|
width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Education |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">A |
||||
|
platform for |
||||
|
educational management</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/manufacturing-black.png" class="img-responsive mb-3" height="48px" |
||||
|
width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Manufacturing |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">Plan, |
||||
|
track and |
||||
|
schedule your operations</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/ecom-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
E-commerce & Website |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">Mobile |
||||
|
friendly, |
||||
|
awe-inspiring product pages</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/service-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Service Management |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">Keep |
||||
|
track of |
||||
|
services and invoice</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/restaurant-black.png" class="img-responsive mb-3" height="48px" |
||||
|
width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Restaurant |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">Run |
||||
|
your bar or |
||||
|
restaurant methodically</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-lg-3"> |
||||
|
<div class="my-4 d-flex flex-column justify-content-center" |
||||
|
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
||||
|
<img src="./assets/icons/hotel-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
||||
|
<h5 |
||||
|
style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
||||
|
Hotel Management |
||||
|
</h5> |
||||
|
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;">An |
||||
|
all-inclusive |
||||
|
hotel management application</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<!-- END OF END OF OUR INDUSTRIES --> |
||||
|
|
||||
|
<!-- FOOTER --> |
||||
|
<!-- Footer Section --> |
||||
|
<section class="container" style="margin: 5rem auto 2rem; background-color: #fff !important;"> |
||||
|
<div class="row" style="max-width:1540px;"> |
||||
|
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mb-4"> |
||||
|
|
||||
|
<h2 |
||||
|
style="font-weight: 300 !important; background-color: #fff !important; z-index: 1 !important; padding: 0 1rem !important;"> |
||||
|
Need Help?</h2> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- Contact Cards --> |
||||
|
<div class="row d-flex justify-content-center align-items-center" |
||||
|
style="max-width:1540px; margin: 0 auto 2rem auto;"> |
||||
|
<div class="col-lg-12" style="padding: 0rem 3rem 2rem; border-radius: 10px; margin-right: 3rem; "> |
||||
|
<div class="row mt-4"> |
||||
|
<div class="col-lg-6"> |
||||
|
<a href="mailto:odoo@cybrosys.com" target="_blank" class="btn btn-block mb-2 deep_hover" |
||||
|
style="text-decoration: none; background-color: #4d4d4d; color: #FFF; border-radius: 4px;"><i |
||||
|
class="fa fa-envelope mr-2"></i>odoo@cybrosys.com</a> |
||||
|
</div> |
||||
|
<div class="col-lg-6"> |
||||
|
<a href="https://api.whatsapp.com/send?phone=918606827707" target="_blank" |
||||
|
class="btn btn-block mb-2 deep_hover" |
||||
|
style="text-decoration: none; background-color: #25D366; color: #FFF; border-radius: 4px;"> |
||||
|
<i class="fa fa-whatsapp mr-2"></i>+91 86068 27707</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- End of Contact Cards --> |
||||
|
</section> |
||||
|
<!-- Footer --> |
||||
|
<section class="oe_container" style="padding: 2rem 3rem 1rem; background-color: #fff !important;"> |
||||
|
<div class="row" style="max-width:1540px; margin: 0 auto; margin-right: 3rem; "> |
||||
|
<!-- Logo --> |
||||
|
<div class="col-lg-12 d-flex justify-content-center align-items-center" style="margin-top: 3rem;"> |
||||
|
<img src="https://www.cybrosys.com/images/logo.png" width="200px" height="auto" /> |
||||
|
</div> |
||||
|
<!-- End of Logo --> |
||||
|
</div> |
||||
|
</section> |
||||
|
<!-- END OF FOOTER --> |
||||
|
</div> |
After Width: | Height: | Size: 717 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 446 B |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 657 B |
After Width: | Height: | Size: 700 B |
After Width: | Height: | Size: 644 B |
After Width: | Height: | Size: 656 B |
After Width: | Height: | Size: 874 B |
After Width: | Height: | Size: 445 B |
After Width: | Height: | Size: 730 B |
After Width: | Height: | Size: 718 B |
After Width: | Height: | Size: 755 B |
After Width: | Height: | Size: 539 B |
After Width: | Height: | Size: 709 B |
After Width: | Height: | Size: 762 B |
After Width: | Height: | Size: 607 B |
After Width: | Height: | Size: 620 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 621 B |
After Width: | Height: | Size: 367 B |
After Width: | Height: | Size: 616 B |
After Width: | Height: | Size: 570 B |
After Width: | Height: | Size: 597 B |
After Width: | Height: | Size: 729 B |
After Width: | Height: | Size: 752 B |
After Width: | Height: | Size: 669 B |
After Width: | Height: | Size: 509 B |
After Width: | Height: | Size: 623 B |
After Width: | Height: | Size: 695 B |
After Width: | Height: | Size: 626 B |
After Width: | Height: | Size: 882 B |
After Width: | Height: | Size: 505 B |
After Width: | Height: | Size: 527 B |
After Width: | Height: | Size: 571 B |
After Width: | Height: | Size: 616 B |
After Width: | Height: | Size: 697 B |
@ -0,0 +1,93 @@ |
|||||
|
odoo.define('code_backend_theme.SidebarMenu', function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
|
//sidebar toggle effect
|
||||
|
$(document).on("click", "#closeSidebar", function(event){ |
||||
|
$("#closeSidebar").hide(); |
||||
|
$("#openSidebar").show(); |
||||
|
}); |
||||
|
$(document).on("click", "#openSidebar", function(event){ |
||||
|
$("#openSidebar").hide(); |
||||
|
$("#closeSidebar").show(); |
||||
|
}); |
||||
|
$(document).on("click", "#openSidebar", function(event){ |
||||
|
$("#sidebar_panel").css({'display':'block'}); |
||||
|
$(".o_action_manager").css({'margin-left': '200px','transition':'all .1s linear'}); |
||||
|
$(".top_heading").css({'margin-left': '200px','transition':'all .1s linear'}); |
||||
|
|
||||
|
//add class in navbar
|
||||
|
var navbar = $(".o_main_navbar"); |
||||
|
var navbar_id = navbar.data("id"); |
||||
|
$("nav").addClass(navbar_id); |
||||
|
navbar.addClass("small_nav"); |
||||
|
|
||||
|
//add class in action-manager
|
||||
|
var action_manager = $(".o_action_manager"); |
||||
|
var action_manager_id = action_manager.data("id"); |
||||
|
$("div").addClass(action_manager_id); |
||||
|
action_manager.addClass("sidebar_margin"); |
||||
|
|
||||
|
//add class in top_heading
|
||||
|
var top_head = $(".top_heading"); |
||||
|
var top_head_id = top_head.data("id"); |
||||
|
$("div").addClass(top_head_id); |
||||
|
top_head.addClass("sidebar_margin"); |
||||
|
}); |
||||
|
$(document).on("click", "#closeSidebar", function(event){ |
||||
|
$("#sidebar_panel").css({'display':'none'}); |
||||
|
$(".o_action_manager").css({'margin-left': '0px'}); |
||||
|
$(".top_heading").css({'margin-left': '0px'}); |
||||
|
|
||||
|
//remove class in navbar
|
||||
|
var navbar = $(".o_main_navbar"); |
||||
|
var navbar_id = navbar.data("id"); |
||||
|
$("nav").removeClass(navbar_id); |
||||
|
navbar.removeClass("small_nav"); |
||||
|
|
||||
|
//remove class in action-manager
|
||||
|
var action_manager = $(".o_action_manager"); |
||||
|
var action_manager_id = action_manager.data("id"); |
||||
|
$("div").removeClass(action_manager_id); |
||||
|
action_manager.removeClass("sidebar_margin"); |
||||
|
|
||||
|
//remove class in top_heading
|
||||
|
var top_head = $(".top_heading"); |
||||
|
var top_head_id = top_head.data("id"); |
||||
|
$("div").removeClass(top_head_id); |
||||
|
top_head.removeClass("sidebar_margin"); |
||||
|
}); |
||||
|
|
||||
|
$(document).on("click", ".sidebar a", function(event){ |
||||
|
var menu = $(".sidebar a"); |
||||
|
var $this = $(this); |
||||
|
var id = $this.data("id"); |
||||
|
$("header").removeClass().addClass(id); |
||||
|
menu.removeClass("active"); |
||||
|
$this.addClass("active"); |
||||
|
|
||||
|
//sidebar close on menu-item click
|
||||
|
$("#sidebar_panel").css({'display':'none'}); |
||||
|
$(".o_action_manager").css({'margin-left': '0px'}); |
||||
|
$(".top_heading").css({'margin-left': '0px'}); |
||||
|
$("#closeSidebar").hide(); |
||||
|
$("#openSidebar").show(); |
||||
|
|
||||
|
//remove class in navbar
|
||||
|
var navbar = $(".o_main_navbar"); |
||||
|
var navbar_id = navbar.data("id"); |
||||
|
$("nav").removeClass(navbar_id); |
||||
|
navbar.removeClass("small_nav"); |
||||
|
|
||||
|
//remove class in action-manager
|
||||
|
var action_manager = $(".o_action_manager"); |
||||
|
var action_manager_id = action_manager.data("id"); |
||||
|
$("div").removeClass(action_manager_id); |
||||
|
action_manager.removeClass("sidebar_margin"); |
||||
|
|
||||
|
//remove class in top_heading
|
||||
|
var top_head = $(".top_heading"); |
||||
|
var top_head_id = top_head.data("id"); |
||||
|
$("div").removeClass(top_head_id); |
||||
|
top_head.removeClass("sidebar_margin"); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,32 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
export const COLORS = ["#556ee6", "#f1b44c", "#50a5f1", "#ffbb78", "#34c38f", "#98df8a", "#d62728", |
||||
|
"#ff9896", "#9467bd", "#c5b0d5", "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", |
||||
|
"#7f7f7f", "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"]; |
||||
|
|
||||
|
/** |
||||
|
* @param {number} index |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
export function getColor(index) { |
||||
|
return COLORS[index % COLORS.length]; |
||||
|
} |
||||
|
|
||||
|
export const DEFAULT_BG = "#d3d3d3"; |
||||
|
|
||||
|
export const BORDER_WHITE = "rgba(255,255,255,0.6)"; |
||||
|
|
||||
|
const RGB_REGEX = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; |
||||
|
|
||||
|
/** |
||||
|
* @param {string} hex |
||||
|
* @param {number} opacity |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
export function hexToRGBA(hex, opacity) { |
||||
|
const rgb = RGB_REGEX.exec(hex) |
||||
|
.slice(1, 4) |
||||
|
.map((n) => parseInt(n, 16)) |
||||
|
.join(","); |
||||
|
return `rgba(${rgb},${opacity})`; |
||||
|
} |
@ -0,0 +1,82 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
import { evaluateExpr } from "@web/core/py_js/py"; |
||||
|
import { GROUPABLE_TYPES } from "@web/search/utils/misc"; |
||||
|
import { XMLParser } from "@web/core/utils/xml"; |
||||
|
import { archParseBoolean } from "@web/views/helpers/utils"; |
||||
|
|
||||
|
const MODES = ["bar", "line", "pie"]; |
||||
|
const ORDERS = ["ASC", "DESC", null]; |
||||
|
|
||||
|
export class GraphArchParser extends XMLParser { |
||||
|
parse(arch, fields = {}) { |
||||
|
const archInfo = { fields, fieldAttrs: {}, groupBy: [] }; |
||||
|
this.visitXML(arch, (node) => { |
||||
|
switch (node.tagName) { |
||||
|
case "graph": { |
||||
|
if (node.hasAttribute("disable_linking")) { |
||||
|
archInfo.disableLinking = archParseBoolean( |
||||
|
node.getAttribute("disable_linking") |
||||
|
); |
||||
|
} |
||||
|
if (node.hasAttribute("stacked")) { |
||||
|
archInfo.stacked = archParseBoolean(node.getAttribute("stacked")); |
||||
|
} |
||||
|
const mode = node.getAttribute("type"); |
||||
|
if (mode && MODES.includes(mode)) { |
||||
|
archInfo.mode = mode; |
||||
|
} |
||||
|
const order = node.getAttribute("order"); |
||||
|
if (order && ORDERS.includes(order)) { |
||||
|
archInfo.order = order; |
||||
|
} |
||||
|
const title = node.getAttribute("string"); |
||||
|
if (title) { |
||||
|
archInfo.title = title; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
case "field": { |
||||
|
let fieldName = node.getAttribute("name"); // exists (rng validation)
|
||||
|
if (fieldName === "id") { |
||||
|
break; |
||||
|
} |
||||
|
const string = node.getAttribute("string"); |
||||
|
if (string) { |
||||
|
if (!archInfo.fieldAttrs[fieldName]) { |
||||
|
archInfo.fieldAttrs[fieldName] = {}; |
||||
|
} |
||||
|
archInfo.fieldAttrs[fieldName].string = string; |
||||
|
} |
||||
|
const isInvisible = Boolean( |
||||
|
evaluateExpr(node.getAttribute("invisible") || "0") |
||||
|
); |
||||
|
if (isInvisible) { |
||||
|
if (!archInfo.fieldAttrs[fieldName]) { |
||||
|
archInfo.fieldAttrs[fieldName] = {}; |
||||
|
} |
||||
|
archInfo.fieldAttrs[fieldName].isInvisible = true; |
||||
|
break; |
||||
|
} |
||||
|
const isMeasure = node.getAttribute("type") === "measure"; |
||||
|
if (isMeasure) { |
||||
|
// the last field with type="measure" (if any) will be used as measure else __count
|
||||
|
archInfo.measure = fieldName; |
||||
|
} else { |
||||
|
const { type } = archInfo.fields[fieldName]; // exists (rng validation)
|
||||
|
if (GROUPABLE_TYPES.includes(type)) { |
||||
|
let groupBy = fieldName; |
||||
|
const interval = node.getAttribute("interval"); |
||||
|
if (interval) { |
||||
|
groupBy += `:${interval}`; |
||||
|
} |
||||
|
archInfo.groupBy.push(groupBy); |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
return archInfo; |
||||
|
} |
||||
|
} |
@ -0,0 +1,537 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
import { sortBy } from "@web/core/utils/arrays"; |
||||
|
import { KeepLast, Race } from "@web/core/utils/concurrency"; |
||||
|
import { rankInterval } from "@web/search/utils/dates"; |
||||
|
import { getGroupBy } from "@web/search/utils/group_by"; |
||||
|
import { GROUPABLE_TYPES } from "@web/search/utils/misc"; |
||||
|
import { Model } from "@web/views/helpers/model"; |
||||
|
import { computeReportMeasures, processMeasure } from "@web/views/helpers/utils"; |
||||
|
|
||||
|
export const SEP = " / "; |
||||
|
|
||||
|
/** |
||||
|
* @typedef {import("@web/search/search_model").SearchParams} SearchParams |
||||
|
*/ |
||||
|
|
||||
|
class DateClasses { |
||||
|
// We view the param "array" as a matrix of values and undefined.
|
||||
|
// An equivalence class is formed of defined values of a column.
|
||||
|
// So nothing has to do with dates but we only use Dateclasses to manage
|
||||
|
// identification of dates.
|
||||
|
/** |
||||
|
* @param {(any[])[]} array |
||||
|
*/ |
||||
|
constructor(array) { |
||||
|
this.__referenceIndex = null; |
||||
|
this.__array = array; |
||||
|
for (let i = 0; i < this.__array.length; i++) { |
||||
|
const arr = this.__array[i]; |
||||
|
if (arr.length && this.__referenceIndex === null) { |
||||
|
this.__referenceIndex = i; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {number} index |
||||
|
* @param {any} o |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
classLabel(index, o) { |
||||
|
return `${this.__array[index].indexOf(o)}`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} classLabel |
||||
|
* @returns {any[]} |
||||
|
*/ |
||||
|
classMembers(classLabel) { |
||||
|
const classNumber = Number(classLabel); |
||||
|
const classMembers = new Set(); |
||||
|
for (const arr of this.__array) { |
||||
|
if (arr[classNumber] !== undefined) { |
||||
|
classMembers.add(arr[classNumber]); |
||||
|
} |
||||
|
} |
||||
|
return [...classMembers]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} classLabel |
||||
|
* @param {number} [index] |
||||
|
* @returns {any} |
||||
|
*/ |
||||
|
representative(classLabel, index) { |
||||
|
const classNumber = Number(classLabel); |
||||
|
const i = index === undefined ? this.__referenceIndex : index; |
||||
|
if (i === null) { |
||||
|
return null; |
||||
|
} |
||||
|
return this.__array[i][classNumber]; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {number} index |
||||
|
* @returns {number} |
||||
|
*/ |
||||
|
arrayLength(index) { |
||||
|
return this.__array[index].length; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class GraphModel extends Model { |
||||
|
/** |
||||
|
* @override |
||||
|
*/ |
||||
|
setup(params) { |
||||
|
// concurrency management
|
||||
|
this.keepLast = new KeepLast(); |
||||
|
this.race = new Race(); |
||||
|
const _fetchDataPoints = this._fetchDataPoints.bind(this); |
||||
|
this._fetchDataPoints = (...args) => { |
||||
|
return this.race.add(_fetchDataPoints(...args)); |
||||
|
}; |
||||
|
|
||||
|
this.initialGroupBy = null; |
||||
|
|
||||
|
this.metaData = params; |
||||
|
this.data = null; |
||||
|
this.searchParams = null; |
||||
|
} |
||||
|
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
// Public
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
|
||||
|
/** |
||||
|
* @param {SearchParams} searchParams |
||||
|
*/ |
||||
|
async load(searchParams) { |
||||
|
this.searchParams = searchParams; |
||||
|
if (!this.initialGroupBy) { |
||||
|
this.initialGroupBy = searchParams.context.graph_groupbys || this.metaData.groupBy; // = arch groupBy --> change that
|
||||
|
} |
||||
|
const metaData = this._buildMetaData(); |
||||
|
return this._fetchDataPoints(metaData); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @override |
||||
|
*/ |
||||
|
hasData() { |
||||
|
return this.dataPoints.length > 0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Only supposed to be called to change one or several parameters among |
||||
|
* "measure", "mode", "order", and "stacked". |
||||
|
* @param {Object} params |
||||
|
*/ |
||||
|
async updateMetaData(params) { |
||||
|
if ("measure" in params) { |
||||
|
const metaData = this._buildMetaData(params); |
||||
|
await this._fetchDataPoints(metaData); |
||||
|
} else { |
||||
|
await this.race.getCurrentProm(); |
||||
|
this.metaData = Object.assign({}, this.metaData, params); |
||||
|
this._prepareData(); |
||||
|
} |
||||
|
this.notify(); |
||||
|
} |
||||
|
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
// Protected
|
||||
|
//--------------------------------------------------------------------------
|
||||
|
|
||||
|
/** |
||||
|
* @protected |
||||
|
* @param {Object} [params={}] |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
_buildMetaData(params) { |
||||
|
const { comparison, domain, context, groupBy } = this.searchParams; |
||||
|
|
||||
|
const metaData = Object.assign({}, this.metaData, { context }); |
||||
|
if (comparison) { |
||||
|
metaData.domains = comparison.domains; |
||||
|
metaData.comparisonField = comparison.fieldName; |
||||
|
} else { |
||||
|
metaData.domains = [{ arrayRepr: domain, description: null }]; |
||||
|
} |
||||
|
metaData.measure = context.graph_measure || metaData.measure; |
||||
|
metaData.mode = context.graph_mode || metaData.mode; |
||||
|
metaData.groupBy = groupBy.length ? groupBy : this.initialGroupBy; |
||||
|
|
||||
|
this._normalize(metaData); |
||||
|
|
||||
|
metaData.measures = computeReportMeasures( |
||||
|
metaData.fields, |
||||
|
metaData.fieldAttrs, |
||||
|
[metaData.measure], |
||||
|
metaData.additionalMeasures |
||||
|
); |
||||
|
|
||||
|
return Object.assign(metaData, params); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetch the data points determined by the metaData. This function has |
||||
|
* several side effects. It can alter this.metaData and set this.dataPoints. |
||||
|
* @protected |
||||
|
* @param {Object} metaData |
||||
|
*/ |
||||
|
async _fetchDataPoints(metaData) { |
||||
|
this.dataPoints = await this.keepLast.add(this._loadDataPoints(metaData)); |
||||
|
this.metaData = metaData; |
||||
|
this._prepareData(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Separates dataPoints coming from the read_group(s) into different |
||||
|
* datasets. This function returns the parameters data and labels used |
||||
|
* to produce the charts. |
||||
|
* @protected |
||||
|
* @param {Object[]} |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
_getData(dataPoints) { |
||||
|
const { comparisonField, groupBy, mode } = this.metaData; |
||||
|
|
||||
|
let identify = false; |
||||
|
if (comparisonField && groupBy.length && groupBy[0].fieldName === comparisonField) { |
||||
|
identify = true; |
||||
|
} |
||||
|
const dateClasses = identify ? this._getDateClasses(dataPoints) : null; |
||||
|
|
||||
|
// dataPoints --> labels
|
||||
|
let labels = []; |
||||
|
const labelMap = {}; |
||||
|
for (const dataPt of dataPoints) { |
||||
|
const x = dataPt.labels.slice(0, mode === "pie" ? undefined : 1); |
||||
|
const trueLabel = x.length ? x.join(SEP) : this.env._t("Total"); |
||||
|
if (dateClasses) { |
||||
|
x[0] = dateClasses.classLabel(dataPt.originIndex, x[0]); |
||||
|
} |
||||
|
const key = JSON.stringify(x); |
||||
|
if (labelMap[key] === undefined) { |
||||
|
labelMap[key] = labels.length; |
||||
|
if (dateClasses) { |
||||
|
if (mode === "pie") { |
||||
|
x[0] = dateClasses.classMembers(x[0]).join(", "); |
||||
|
} else { |
||||
|
x[0] = dateClasses.representative(x[0]); |
||||
|
} |
||||
|
} |
||||
|
const label = x.length ? x.join(SEP) : this.env._t("Total"); |
||||
|
labels.push(label); |
||||
|
} |
||||
|
dataPt.labelIndex = labelMap[key]; |
||||
|
dataPt.trueLabel = trueLabel; |
||||
|
} |
||||
|
|
||||
|
// dataPoints + labels --> datasetsTmp --> datasets
|
||||
|
const datasetsTmp = {}; |
||||
|
for (const dataPt of dataPoints) { |
||||
|
const { domain, labelIndex, originIndex, trueLabel, value } = dataPt; |
||||
|
const datasetLabel = this._getDatasetLabel(dataPt); |
||||
|
if (!(datasetLabel in datasetsTmp)) { |
||||
|
let dataLength = labels.length; |
||||
|
if (mode !== "pie" && dateClasses) { |
||||
|
dataLength = dateClasses.arrayLength(originIndex); |
||||
|
} |
||||
|
datasetsTmp[datasetLabel] = { |
||||
|
data: new Array(dataLength).fill(0), |
||||
|
trueLabels: labels.slice(0, dataLength), // should be good // check this in case identify = true
|
||||
|
domains: new Array(dataLength).fill([]), |
||||
|
label: datasetLabel, |
||||
|
originIndex: originIndex, |
||||
|
}; |
||||
|
} |
||||
|
datasetsTmp[datasetLabel].data[labelIndex] = value; |
||||
|
datasetsTmp[datasetLabel].domains[labelIndex] = domain; |
||||
|
datasetsTmp[datasetLabel].trueLabels[labelIndex] = trueLabel; |
||||
|
} |
||||
|
// sort by origin
|
||||
|
let datasets = sortBy(Object.values(datasetsTmp), "originIndex"); |
||||
|
|
||||
|
if (mode === "pie") { |
||||
|
// We kinda have a matrix. We remove the zero columns and rows. This is a global operation.
|
||||
|
// That's why it cannot be done before.
|
||||
|
datasets = datasets.filter((dataset) => dataset.data.some((v) => Boolean(v))); |
||||
|
const labelsToKeepIndexes = {}; |
||||
|
labels.forEach((_, index) => { |
||||
|
if (datasets.some((dataset) => Boolean(dataset.data[index]))) { |
||||
|
labelsToKeepIndexes[index] = true; |
||||
|
} |
||||
|
}); |
||||
|
labels = labels.filter((_, index) => labelsToKeepIndexes[index]); |
||||
|
for (const dataset of datasets) { |
||||
|
dataset.data = dataset.data.filter((_, index) => labelsToKeepIndexes[index]); |
||||
|
dataset.domains = dataset.domains.filter((_, index) => labelsToKeepIndexes[index]); |
||||
|
dataset.trueLabels = dataset.trueLabels.filter( |
||||
|
(_, index) => labelsToKeepIndexes[index] |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return { datasets, labels }; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Determines the dataset to which the data point belongs. |
||||
|
* @protected |
||||
|
* @param {Object} dataPoint |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
_getDatasetLabel(dataPoint) { |
||||
|
const { measure, measures, domains, mode } = this.metaData; |
||||
|
const { labels, originIndex } = dataPoint; |
||||
|
if (mode === "pie") { |
||||
|
return domains[originIndex].description || ""; |
||||
|
} |
||||
|
// ([origin] + second to last groupBys) or measure
|
||||
|
let datasetLabel = labels.slice(1).join(SEP); |
||||
|
if (domains.length > 1) { |
||||
|
datasetLabel = |
||||
|
domains[originIndex].description + (datasetLabel ? SEP + datasetLabel : ""); |
||||
|
} |
||||
|
datasetLabel = datasetLabel || measures[measure].string; |
||||
|
return datasetLabel; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @protected |
||||
|
* @param {Object[]} dataPoints |
||||
|
* @returns {DateClasses} |
||||
|
*/ |
||||
|
_getDateClasses(dataPoints) { |
||||
|
const { domains } = this.metaData; |
||||
|
const dateSets = domains.map(() => new Set()); |
||||
|
for (const { labels, originIndex } of dataPoints) { |
||||
|
const date = labels[0]; |
||||
|
dateSets[originIndex].add(date); |
||||
|
} |
||||
|
const arrays = dateSets.map((dateSet) => [...dateSet]); |
||||
|
return new DateClasses(arrays); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Eventually filters and sort data points. |
||||
|
* @protected |
||||
|
* @returns {Object[]} |
||||
|
*/ |
||||
|
_getProcessedDataPoints() { |
||||
|
const { domains, groupBy, mode, order } = this.metaData; |
||||
|
let processedDataPoints = []; |
||||
|
if (mode === "line") { |
||||
|
processedDataPoints = this.dataPoints.filter( |
||||
|
(dataPoint) => dataPoint.labels[0] !== this.env._t("Undefined") |
||||
|
); |
||||
|
} else { |
||||
|
processedDataPoints = this.dataPoints.filter((dataPoint) => dataPoint.count !== 0); |
||||
|
} |
||||
|
|
||||
|
if (order !== null && mode !== "pie" && domains.length === 1 && groupBy.length > 0) { |
||||
|
// group data by their x-axis value, and then sort datapoints
|
||||
|
// based on the sum of values by group in ascending/descending order
|
||||
|
const groupedDataPoints = {}; |
||||
|
for (const dataPt of processedDataPoints) { |
||||
|
const key = dataPt.labels[0]; // = x-axis value under the current assumptions
|
||||
|
if (!groupedDataPoints[key]) { |
||||
|
groupedDataPoints[key] = []; |
||||
|
} |
||||
|
groupedDataPoints[key].push(dataPt); |
||||
|
} |
||||
|
const groups = Object.values(groupedDataPoints); |
||||
|
const groupTotal = (group) => group.reduce((sum, dataPt) => sum + dataPt.value, 0); |
||||
|
processedDataPoints = sortBy(groups, groupTotal, order.toLowerCase()).flat(); |
||||
|
} |
||||
|
|
||||
|
return processedDataPoints; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Determines whether the set of data points is good. If not, this.data will be (re)set to null |
||||
|
* @protected |
||||
|
* @param {Object[]} |
||||
|
* @returns {boolean} |
||||
|
*/ |
||||
|
_isValidData(dataPoints) { |
||||
|
const { mode } = this.metaData; |
||||
|
let somePositive = false; |
||||
|
let someNegative = false; |
||||
|
if (mode === "pie") { |
||||
|
for (const dataPt of dataPoints) { |
||||
|
if (dataPt.value > 0) { |
||||
|
somePositive = true; |
||||
|
} else if (dataPt.value < 0) { |
||||
|
someNegative = true; |
||||
|
} |
||||
|
} |
||||
|
if (someNegative && somePositive) { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Fetch and process graph data. It is basically a(some) read_group(s) |
||||
|
* with correct fields for each domain. We have to do some light processing |
||||
|
* to separate date groups in the field list, because they can be defined |
||||
|
* with an aggregation function, such as my_date:week. |
||||
|
* @protected |
||||
|
* @param {Object} metaData |
||||
|
* @returns {Object[]} |
||||
|
*/ |
||||
|
async _loadDataPoints(metaData) { |
||||
|
const { measure, domains, fields, groupBy, resModel } = metaData; |
||||
|
|
||||
|
const measures = ["__count"]; |
||||
|
if (measure !== "__count") { |
||||
|
let { group_operator, type } = fields[measure]; |
||||
|
if (type === "many2one") { |
||||
|
group_operator = "count_distinct"; |
||||
|
} |
||||
|
if (group_operator === undefined) { |
||||
|
throw new Error( |
||||
|
`No aggregate function has been provided for the measure '${measure}'` |
||||
|
); |
||||
|
} |
||||
|
measures.push(`${measure}:${group_operator}`); |
||||
|
} |
||||
|
|
||||
|
const proms = []; |
||||
|
const numbering = {}; // used to avoid ambiguity with many2one with values with same labels:
|
||||
|
// for instance [1, "ABC"] [3, "ABC"] should be distinguished.
|
||||
|
domains.forEach((domain, originIndex) => { |
||||
|
proms.push( |
||||
|
this.orm |
||||
|
.webReadGroup( |
||||
|
resModel, |
||||
|
domain.arrayRepr, |
||||
|
measures, |
||||
|
groupBy.map((gb) => gb.spec), |
||||
|
{ lazy: false }, // what is this thing???
|
||||
|
{ fill_temporal: true, ...this.searchParams.context } |
||||
|
) |
||||
|
.then((data) => { |
||||
|
const dataPoints = []; |
||||
|
for (const group of data.groups) { |
||||
|
const { __domain, __count } = group; |
||||
|
const labels = []; |
||||
|
|
||||
|
for (const gb of groupBy) { |
||||
|
let label; |
||||
|
const val = group[gb.spec]; |
||||
|
const fieldName = gb.fieldName; |
||||
|
const { type } = fields[fieldName]; |
||||
|
if (type === "boolean") { |
||||
|
label = `${val}`; // toUpperCase?
|
||||
|
} else if (val === false) { |
||||
|
label = this.env._t("Undefined"); |
||||
|
} else if (type === "many2one") { |
||||
|
const [id, name] = val; |
||||
|
const key = JSON.stringify([fieldName, name]); |
||||
|
if (!numbering[key]) { |
||||
|
numbering[key] = {}; |
||||
|
} |
||||
|
const numbers = numbering[key]; |
||||
|
if (!numbers[id]) { |
||||
|
numbers[id] = Object.keys(numbers).length + 1; |
||||
|
} |
||||
|
const num = numbers[id]; |
||||
|
label = num === 1 ? name : `${name} (${num})`; |
||||
|
} else if (type === "selection") { |
||||
|
const selected = fields[fieldName].selection.find( |
||||
|
(s) => s[0] === val |
||||
|
); |
||||
|
label = selected[1]; |
||||
|
} else { |
||||
|
label = val; |
||||
|
} |
||||
|
labels.push(label); |
||||
|
} |
||||
|
|
||||
|
let value = group[measure]; |
||||
|
if (value instanceof Array) { |
||||
|
// case where measure is a many2one and is used as groupBy
|
||||
|
value = 1; |
||||
|
} |
||||
|
if (!Number.isInteger(value)) { |
||||
|
metaData.allIntegers = false; |
||||
|
} |
||||
|
dataPoints.push({ |
||||
|
count: __count, |
||||
|
domain: __domain, |
||||
|
value, |
||||
|
labels, |
||||
|
originIndex, |
||||
|
}); |
||||
|
} |
||||
|
return dataPoints; |
||||
|
}) |
||||
|
); |
||||
|
}); |
||||
|
const promResults = await Promise.all(proms); |
||||
|
return promResults.flat(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Process metaData.groupBy in order to keep only the finest interval option for |
||||
|
* elements based on date/datetime field (e.g. 'date:year'). This means that |
||||
|
* 'week' is prefered to 'month'. The field stays at the place of its first occurence. |
||||
|
* For instance, |
||||
|
* ['foo', 'date:month', 'bar', 'date:week'] becomes ['foo', 'date:week', 'bar']. |
||||
|
* @protected |
||||
|
* @param {Object} metaData |
||||
|
*/ |
||||
|
_normalize(metaData) { |
||||
|
const { fields } = metaData; |
||||
|
const groupBy = []; |
||||
|
for (const gb of metaData.groupBy) { |
||||
|
let ngb = gb; |
||||
|
if (typeof gb === "string") { |
||||
|
ngb = getGroupBy(gb, fields); |
||||
|
} |
||||
|
groupBy.push(ngb); |
||||
|
} |
||||
|
|
||||
|
const processedGroupBy = []; |
||||
|
for (const gb of groupBy) { |
||||
|
const { fieldName, interval } = gb; |
||||
|
const { store, type } = fields[fieldName]; |
||||
|
if ( |
||||
|
!store || |
||||
|
["id", "__count"].includes(fieldName) || |
||||
|
!GROUPABLE_TYPES.includes(type) |
||||
|
) { |
||||
|
continue; |
||||
|
} |
||||
|
const index = processedGroupBy.findIndex((gb) => gb.fieldName === fieldName); |
||||
|
if (index === -1) { |
||||
|
processedGroupBy.push(gb); |
||||
|
} else if (interval) { |
||||
|
const registeredInterval = processedGroupBy[index].interval; |
||||
|
if (rankInterval(registeredInterval) < rankInterval(interval)) { |
||||
|
processedGroupBy.splice(index, 1, gb); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
metaData.groupBy = processedGroupBy; |
||||
|
|
||||
|
metaData.measure = processMeasure(metaData.measure); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @protected |
||||
|
*/ |
||||
|
async _prepareData() { |
||||
|
const processedDataPoints = this._getProcessedDataPoints(); |
||||
|
this.data = null; |
||||
|
if (this._isValidData(processedDataPoints)) { |
||||
|
this.data = this._getData(processedDataPoints); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,627 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
import { _lt } from "@web/core/l10n/translation"; |
||||
|
import { BORDER_WHITE, DEFAULT_BG, getColor, hexToRGBA } from "./colors"; |
||||
|
import { formatFloat } from "@web/fields/formatters"; |
||||
|
import { SEP } from "./graph_model"; |
||||
|
import { sortBy } from "@web/core/utils/arrays"; |
||||
|
import { useAssets } from "@web/core/assets"; |
||||
|
import { useEffect } from "@web/core/utils/hooks"; |
||||
|
|
||||
|
const { Component, hooks } = owl; |
||||
|
const { useRef } = hooks; |
||||
|
|
||||
|
const NO_DATA = _lt("No data"); |
||||
|
|
||||
|
/** |
||||
|
* @param {Object} chartArea |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
function getMaxWidth(chartArea) { |
||||
|
const { left, right } = chartArea; |
||||
|
return Math.floor((right - left) / 1.618) + "px"; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Used to avoid too long legend items. |
||||
|
* @param {string|Strin} label |
||||
|
* @returns {string} shortened version of the input label |
||||
|
*/ |
||||
|
function shortenLabel(label) { |
||||
|
// string returned could be wrong if a groupby value contain a " / "!
|
||||
|
const groups = label.toString().split(SEP); |
||||
|
let shortLabel = groups.slice(0, 3).join(SEP); |
||||
|
if (shortLabel.length > 30) { |
||||
|
shortLabel = `${shortLabel.slice(0, 30)}...`; |
||||
|
} else if (groups.length > 3) { |
||||
|
shortLabel = `${shortLabel}${SEP}...`; |
||||
|
} |
||||
|
return shortLabel; |
||||
|
} |
||||
|
|
||||
|
export class GraphRenderer extends Component { |
||||
|
setup() { |
||||
|
this.model = this.props.model; |
||||
|
|
||||
|
this.canvasRef = useRef("canvas"); |
||||
|
this.containerRef = useRef("container"); |
||||
|
|
||||
|
this.chart = null; |
||||
|
this.tooltip = null; |
||||
|
this.legendTooltip = null; |
||||
|
|
||||
|
useAssets({ jsLibs: ["/web/static/lib/Chart/Chart.js"] }); |
||||
|
|
||||
|
useEffect(() => this.renderChart()); |
||||
|
} |
||||
|
|
||||
|
willUnmount() { |
||||
|
if (this.chart) { |
||||
|
this.chart.destroy(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* This function aims to remove a suitable number of lines from the |
||||
|
* tooltip in order to make it reasonably visible. A message indicating |
||||
|
* the number of lines is added if necessary. |
||||
|
* @param {HTMLElement} tooltip |
||||
|
* @param {number} maxTooltipHeight this the max height in pixels of the tooltip |
||||
|
*/ |
||||
|
adjustTooltipHeight(tooltip, maxTooltipHeight) { |
||||
|
const sizeOneLine = tooltip.querySelector("tbody tr").clientHeight; |
||||
|
const tbodySize = tooltip.querySelector("tbody").clientHeight; |
||||
|
const toKeep = Math.max( |
||||
|
0, |
||||
|
Math.floor((maxTooltipHeight - (tooltip.clientHeight - tbodySize)) / sizeOneLine) - 1 |
||||
|
); |
||||
|
const lines = tooltip.querySelectorAll("tbody tr"); |
||||
|
const toRemove = lines.length - toKeep; |
||||
|
if (toRemove > 0) { |
||||
|
for (let index = toKeep; index < lines.length; ++index) { |
||||
|
lines[index].remove(); |
||||
|
} |
||||
|
const tr = document.createElement("tr"); |
||||
|
const td = document.createElement("td"); |
||||
|
tr.classList.add("o_show_more"); |
||||
|
td.innerText = this.env._t("..."); |
||||
|
tr.appendChild(td); |
||||
|
tooltip.querySelector("tbody").appendChild(tr); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Creates a custom HTML tooltip. |
||||
|
* @param {Object} data |
||||
|
* @param {Object} metaData |
||||
|
* @param {Object} tooltipModel see chartjs documentation |
||||
|
*/ |
||||
|
customTooltip(data, metaData, tooltipModel) { |
||||
|
const { measure, measures, disableLinking, mode } = metaData; |
||||
|
this.el.style.cursor = ""; |
||||
|
this.removeTooltips(); |
||||
|
if (tooltipModel.opacity === 0 || tooltipModel.dataPoints.length === 0) { |
||||
|
return; |
||||
|
} |
||||
|
if (!disableLinking && mode !== "line") { |
||||
|
this.el.style.cursor = "pointer"; |
||||
|
} |
||||
|
const chartAreaTop = this.chart.chartArea.top; |
||||
|
const viewContentTop = this.el.getBoundingClientRect().top; |
||||
|
const innerHTML = this.env.qweb.renderToString("web.GraphRenderer.CustomTooltip", { |
||||
|
maxWidth: getMaxWidth(this.chart.chartArea), |
||||
|
measure: measures[measure].string, |
||||
|
tooltipItems: this.getTooltipItems(data, metaData, tooltipModel), |
||||
|
}); |
||||
|
const template = Object.assign(document.createElement("template"), { innerHTML }); |
||||
|
const tooltip = template.content.firstChild; |
||||
|
this.containerRef.el.prepend(tooltip); |
||||
|
|
||||
|
let top; |
||||
|
const tooltipHeight = tooltip.clientHeight; |
||||
|
const minTopAllowed = Math.floor(chartAreaTop); |
||||
|
const maxTopAllowed = Math.floor(window.innerHeight - (viewContentTop + tooltipHeight)) - 2; |
||||
|
const y = Math.floor(tooltipModel.y); |
||||
|
if (minTopAllowed <= maxTopAllowed) { |
||||
|
// Here we know that the full tooltip can fit in the screen.
|
||||
|
// We put it in the position where Chart.js would put it
|
||||
|
// if two conditions are respected:
|
||||
|
// 1: the tooltip is not cut (because we know it is possible to not cut it)
|
||||
|
// 2: the tooltip does not hide the legend.
|
||||
|
// If it is not possible to use the Chart.js proposition (y)
|
||||
|
// we use the best approximated value.
|
||||
|
if (y <= maxTopAllowed) { |
||||
|
if (y >= minTopAllowed) { |
||||
|
top = y; |
||||
|
} else { |
||||
|
top = minTopAllowed; |
||||
|
} |
||||
|
} else { |
||||
|
top = maxTopAllowed; |
||||
|
} |
||||
|
} else { |
||||
|
// Here we know that we cannot satisfy condition 1 above,
|
||||
|
// so we position the tooltip at the minimal position and
|
||||
|
// cut it the minimum possible.
|
||||
|
top = minTopAllowed; |
||||
|
const maxTooltipHeight = window.innerHeight - (viewContentTop + chartAreaTop) - 2; |
||||
|
this.adjustTooltipHeight(tooltip, maxTooltipHeight); |
||||
|
} |
||||
|
this.fixTooltipLeftPosition(tooltip, tooltipModel.x); |
||||
|
tooltip.style.top = Math.floor(top) + "px"; |
||||
|
|
||||
|
this.tooltip = tooltip; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Sets best left position of a tooltip approaching the proposal x. |
||||
|
* @param {HTMLElement} tooltip |
||||
|
* @param {number} x |
||||
|
*/ |
||||
|
fixTooltipLeftPosition(tooltip, x) { |
||||
|
let left; |
||||
|
const tooltipWidth = tooltip.clientWidth; |
||||
|
const minLeftAllowed = Math.floor(this.chart.chartArea.left + 2); |
||||
|
const maxLeftAllowed = Math.floor(this.chart.chartArea.right - tooltipWidth - 2); |
||||
|
x = Math.floor(x); |
||||
|
if (x < minLeftAllowed) { |
||||
|
left = minLeftAllowed; |
||||
|
} else if (x > maxLeftAllowed) { |
||||
|
left = maxLeftAllowed; |
||||
|
} else { |
||||
|
left = x; |
||||
|
} |
||||
|
tooltip.style.left = `${left}px`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Used to format correctly the values in tooltips and yAxes. |
||||
|
* @param {number} value |
||||
|
* @param {boolean} [allIntegers=true] |
||||
|
* @returns {string} |
||||
|
*/ |
||||
|
formatValue(value, allIntegers = true) { |
||||
|
const largeNumber = Math.abs(value) >= 1000; |
||||
|
if (allIntegers && !largeNumber) { |
||||
|
return String(value); |
||||
|
} |
||||
|
if (largeNumber) { |
||||
|
return formatFloat(value, { humanReadable: true, decimals: 2, minDigits: 1 }); |
||||
|
} |
||||
|
return formatFloat(value); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the bar chart data |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getBarChartData() { |
||||
|
// style data
|
||||
|
const { domains, stacked } = this.model.metaData; |
||||
|
const data = this.model.data; |
||||
|
for (let index = 0; index < data.datasets.length; ++index) { |
||||
|
const dataset = data.datasets[index]; |
||||
|
// used when stacked
|
||||
|
if (stacked) { |
||||
|
dataset.stack = domains[dataset.originIndex].description || ""; |
||||
|
} |
||||
|
// set dataset color
|
||||
|
dataset.backgroundColor = getColor(index); |
||||
|
} |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the chart config. |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getChartConfig() { |
||||
|
const { mode } = this.model.metaData; |
||||
|
let data; |
||||
|
switch (mode) { |
||||
|
case "bar": |
||||
|
data = this.getBarChartData(); |
||||
|
break; |
||||
|
case "line": |
||||
|
data = this.getLineChartData(); |
||||
|
break; |
||||
|
case "pie": |
||||
|
data = this.getPieChartData(); |
||||
|
} |
||||
|
const options = this.prepareOptions(); |
||||
|
return { data, options, type: mode }; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns an object used to style chart elements independently from |
||||
|
* the datasets. |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getElementOptions() { |
||||
|
const { mode } = this.model.metaData; |
||||
|
const elementOptions = {}; |
||||
|
if (mode === "bar") { |
||||
|
elementOptions.rectangle = { borderWidth: 1 }; |
||||
|
} else if (mode === "line") { |
||||
|
elementOptions.line = { fill: false, tension: 0 }; |
||||
|
} |
||||
|
return elementOptions; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getLegendOptions() { |
||||
|
const { mode } = this.model.metaData; |
||||
|
const data = this.model.data; |
||||
|
const refLength = mode === "pie" ? data.labels.length : data.datasets.length; |
||||
|
const legendOptions = { |
||||
|
display: refLength <= 20, |
||||
|
position: "top", |
||||
|
onHover: this.onlegendHover.bind(this), |
||||
|
onLeave: this.onLegendLeave.bind(this), |
||||
|
}; |
||||
|
if (mode === "line") { |
||||
|
legendOptions.onClick = this.onLegendClick.bind(this); |
||||
|
} |
||||
|
if (mode === "pie") { |
||||
|
legendOptions.labels = { |
||||
|
generateLabels: (chart) => { |
||||
|
const { data } = chart; |
||||
|
const metaData = data.datasets.map( |
||||
|
(_, index) => chart.getDatasetMeta(index).data |
||||
|
); |
||||
|
const labels = data.labels.map((label, index) => { |
||||
|
const hidden = metaData.some((data) => data[index] && data[index].hidden); |
||||
|
const fullText = label; |
||||
|
const text = shortenLabel(fullText); |
||||
|
const fillStyle = label === NO_DATA ? DEFAULT_BG : getColor(index); |
||||
|
return { text, fullText, fillStyle, hidden, index }; |
||||
|
}); |
||||
|
return labels; |
||||
|
}, |
||||
|
}; |
||||
|
} else { |
||||
|
const referenceColor = mode === "bar" ? "backgroundColor" : "borderColor"; |
||||
|
legendOptions.labels = { |
||||
|
generateLabels: (chart) => { |
||||
|
const { data } = chart; |
||||
|
const labels = data.datasets.map((dataset, index) => { |
||||
|
return { |
||||
|
text: shortenLabel(dataset.label), |
||||
|
fullText: dataset.label, |
||||
|
fillStyle: dataset[referenceColor], |
||||
|
hidden: !chart.isDatasetVisible(index), |
||||
|
lineCap: dataset.borderCapStyle, |
||||
|
lineDash: dataset.borderDash, |
||||
|
lineDashOffset: dataset.borderDashOffset, |
||||
|
lineJoin: dataset.borderJoinStyle, |
||||
|
lineWidth: dataset.borderWidth, |
||||
|
strokeStyle: dataset[referenceColor], |
||||
|
pointStyle: dataset.pointStyle, |
||||
|
datasetIndex: index, |
||||
|
}; |
||||
|
}); |
||||
|
return labels; |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
return legendOptions; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns line chart data. |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getLineChartData() { |
||||
|
const { groupBy, domains } = this.model.metaData; |
||||
|
const data = this.model.data; |
||||
|
for (let index = 0; index < data.datasets.length; ++index) { |
||||
|
const dataset = data.datasets[index]; |
||||
|
if (groupBy.length <= 1 && domains.length > 1) { |
||||
|
if (dataset.originIndex === 0) { |
||||
|
dataset.fill = "origin"; |
||||
|
dataset.backgroundColor = hexToRGBA(getColor(0), 0.4); |
||||
|
dataset.borderColor = getColor(0); |
||||
|
} else if (dataset.originIndex === 1) { |
||||
|
dataset.borderColor = getColor(1); |
||||
|
} else { |
||||
|
dataset.borderColor = getColor(index); |
||||
|
} |
||||
|
} else { |
||||
|
dataset.borderColor = getColor(index); |
||||
|
} |
||||
|
if (data.labels.length === 1) { |
||||
|
// shift of the real value to right. This is done to
|
||||
|
// center the points in the chart. See data.labels below in
|
||||
|
// Chart parameters
|
||||
|
dataset.data.unshift(undefined); |
||||
|
dataset.trueLabels.unshift(undefined); |
||||
|
dataset.domains.unshift(undefined); |
||||
|
} |
||||
|
dataset.pointBackgroundColor = dataset.borderColor; |
||||
|
dataset.pointBorderColor = "rgba(0,0,0,0.2)"; |
||||
|
} |
||||
|
if (data.datasets.length === 1) { |
||||
|
const dataset = data.datasets[0]; |
||||
|
dataset.fill = "origin"; |
||||
|
dataset.backgroundColor = hexToRGBA(getColor(0), 0.4); |
||||
|
} |
||||
|
// center the points in the chart (without that code they are put
|
||||
|
// on the left and the graph seems empty)
|
||||
|
data.labels = data.labels.length > 1 ? data.labels : ["", ...data.labels, ""]; |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns pie chart data. |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getPieChartData() { |
||||
|
const { domains } = this.model.metaData; |
||||
|
const data = this.model.data; |
||||
|
// style/complete data
|
||||
|
// give same color to same groups from different origins
|
||||
|
const colors = data.labels.map((_, index) => getColor(index)); |
||||
|
for (const dataset of data.datasets) { |
||||
|
dataset.backgroundColor = colors; |
||||
|
dataset.borderColor = BORDER_WHITE; |
||||
|
} |
||||
|
// make sure there is a zone associated with every origin
|
||||
|
const representedOriginIndexes = new Set( |
||||
|
data.datasets.map((dataset) => dataset.originIndex) |
||||
|
); |
||||
|
let addNoDataToLegend = false; |
||||
|
const fakeData = new Array(data.labels.length + 1); |
||||
|
fakeData[data.labels.length] = 1; |
||||
|
const fakeTrueLabels = new Array(data.labels.length + 1); |
||||
|
fakeTrueLabels[data.labels.length] = NO_DATA; |
||||
|
for (let index = 0; index < domains.length; ++index) { |
||||
|
if (!representedOriginIndexes.has(index)) { |
||||
|
data.datasets.push({ |
||||
|
label: domains[index].description, |
||||
|
data: fakeData, |
||||
|
trueLabels: fakeTrueLabels, |
||||
|
backgroundColor: [...colors, DEFAULT_BG], |
||||
|
borderColor: BORDER_WHITE, |
||||
|
}); |
||||
|
addNoDataToLegend = true; |
||||
|
} |
||||
|
} |
||||
|
if (addNoDataToLegend) { |
||||
|
data.labels.push(NO_DATA); |
||||
|
} |
||||
|
|
||||
|
return data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the options used to generate the chart axes. |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getScaleOptions() { |
||||
|
const { |
||||
|
allIntegers, |
||||
|
displayScaleLabels, |
||||
|
fields, |
||||
|
groupBy, |
||||
|
measure, |
||||
|
measures, |
||||
|
mode, |
||||
|
} = this.model.metaData; |
||||
|
if (mode === "pie") { |
||||
|
return {}; |
||||
|
} |
||||
|
const xAxe = { |
||||
|
type: "category", |
||||
|
scaleLabel: { |
||||
|
display: Boolean(groupBy.length && displayScaleLabels), |
||||
|
labelString: groupBy.length ? fields[groupBy[0].fieldName].string : "", |
||||
|
}, |
||||
|
}; |
||||
|
const yAxe = { |
||||
|
type: "linear", |
||||
|
scaleLabel: { |
||||
|
display: displayScaleLabels, |
||||
|
labelString: measures[measure].string, |
||||
|
}, |
||||
|
ticks: { |
||||
|
callback: (value) => this.formatValue(value, allIntegers), |
||||
|
suggestedMax: 0, |
||||
|
suggestedMin: 0, |
||||
|
}, |
||||
|
}; |
||||
|
return { xAxes: [xAxe], yAxes: [yAxe] }; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* This function extracts the information from the data points in |
||||
|
* tooltipModel.dataPoints (corresponding to datapoints over a given |
||||
|
* label determined by the mouse position) that will be displayed in a |
||||
|
* custom tooltip. |
||||
|
* @param {Object} data |
||||
|
* @param {Object} metaData |
||||
|
* @param {Object} tooltipModel see chartjs documentation |
||||
|
* @returns {Object[]} |
||||
|
*/ |
||||
|
getTooltipItems(data, metaData, tooltipModel) { |
||||
|
const { allIntegers, domains, mode, groupBy } = metaData; |
||||
|
const sortedDataPoints = sortBy(tooltipModel.dataPoints, "yLabel", "desc"); |
||||
|
const items = []; |
||||
|
for (const item of sortedDataPoints) { |
||||
|
const id = item.index; |
||||
|
const dataset = data.datasets[item.datasetIndex]; |
||||
|
let label = dataset.trueLabels[id]; |
||||
|
let value = this.formatValue(dataset.data[id], allIntegers); |
||||
|
let boxColor; |
||||
|
if (mode === "pie") { |
||||
|
if (label === NO_DATA) { |
||||
|
value = this.formatValue(0, allIntegers); |
||||
|
} |
||||
|
if (domains.length > 1) { |
||||
|
label = `${dataset.label} / ${label}`; |
||||
|
} |
||||
|
boxColor = dataset.backgroundColor[id]; |
||||
|
} else { |
||||
|
if (groupBy.length > 1 || domains.length > 1) { |
||||
|
label = `${label} / ${dataset.label}`; |
||||
|
} |
||||
|
boxColor = mode === "bar" ? dataset.backgroundColor : dataset.borderColor; |
||||
|
} |
||||
|
items.push({ id, label, value, boxColor }); |
||||
|
} |
||||
|
return items; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the options used to generate chart tooltips. |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getTooltipOptions() { |
||||
|
const { data, metaData } = this.model; |
||||
|
const { mode } = metaData; |
||||
|
const tooltipOptions = { |
||||
|
enabled: false, |
||||
|
custom: this.customTooltip.bind(this, data, metaData), |
||||
|
}; |
||||
|
if (mode === "line") { |
||||
|
tooltipOptions.mode = "index"; |
||||
|
tooltipOptions.intersect = false; |
||||
|
} |
||||
|
return tooltipOptions; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* If a group has been clicked on, display a view of its records. |
||||
|
* @param {MouseEvent} ev |
||||
|
*/ |
||||
|
onGraphClicked(ev) { |
||||
|
const [activeElement] = this.chart.getElementAtEvent(ev); |
||||
|
if (!activeElement) { |
||||
|
return; |
||||
|
} |
||||
|
const { _datasetIndex, _index } = activeElement; |
||||
|
const { domains } = this.chart.data.datasets[_datasetIndex]; |
||||
|
if (domains) { |
||||
|
this.props.onGraphClicked(domains[_index]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Overrides the default legend 'onClick' behaviour. This is done to |
||||
|
* remove all existing tooltips right before updating the chart. |
||||
|
* @param {Event} ev |
||||
|
* @param {Object} legendItem |
||||
|
*/ |
||||
|
onLegendClick(ev, legendItem) { |
||||
|
this.removeTooltips(); |
||||
|
// Default 'onClick' fallback. See web/static/lib/Chart/Chart.js#15138
|
||||
|
const index = legendItem.datasetIndex; |
||||
|
const meta = this.chart.getDatasetMeta(index); |
||||
|
meta.hidden = meta.hidden === null ? !this.chart.data.datasets[index].hidden : null; |
||||
|
this.chart.update(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* If the text of a legend item has been shortened and the user mouse |
||||
|
* hovers that item (actually the event type is mousemove), a tooltip |
||||
|
* with the item full text is displayed. |
||||
|
* @param {Event} ev |
||||
|
* @param {Object} legendItem |
||||
|
*/ |
||||
|
onlegendHover(ev, legendItem) { |
||||
|
this.canvasRef.el.style.cursor = "pointer"; |
||||
|
/** |
||||
|
* The string legendItem.text is an initial segment of legendItem.fullText. |
||||
|
* If the two coincide, no need to generate a tooltip. If a tooltip |
||||
|
* for the legend already exists, it is already good and does not |
||||
|
* need to be recreated. |
||||
|
*/ |
||||
|
const { fullText, text } = legendItem; |
||||
|
if (this.legendTooltip || text === fullText) { |
||||
|
return; |
||||
|
} |
||||
|
const viewContentTop = this.el.getBoundingClientRect().top; |
||||
|
const legendTooltip = Object.assign(document.createElement("div"), { |
||||
|
className: "o_tooltip_legend", |
||||
|
innerText: fullText, |
||||
|
}); |
||||
|
legendTooltip.style.top = `${ev.clientY - viewContentTop}px`; |
||||
|
legendTooltip.style.maxWidth = getMaxWidth(this.chart.chartArea); |
||||
|
this.containerRef.el.appendChild(legendTooltip); |
||||
|
this.fixTooltipLeftPosition(legendTooltip, ev.clientX); |
||||
|
this.legendTooltip = legendTooltip; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* If there's a legend tooltip and the user mouse out of the |
||||
|
* corresponding legend item, the tooltip is removed. |
||||
|
*/ |
||||
|
onLegendLeave() { |
||||
|
this.canvasRef.el.style.cursor = ""; |
||||
|
this.removeLegendTooltip(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Prepares options for the chart according to the current mode |
||||
|
* (= chart type). This function returns the parameter options used to |
||||
|
* instantiate the chart. |
||||
|
*/ |
||||
|
prepareOptions() { |
||||
|
const { disableLinking, mode } = this.model.metaData; |
||||
|
const options = { |
||||
|
maintainAspectRatio: false, |
||||
|
scales: this.getScaleOptions(), |
||||
|
legend: this.getLegendOptions(), |
||||
|
tooltips: this.getTooltipOptions(), |
||||
|
elements: this.getElementOptions(), |
||||
|
}; |
||||
|
if (!disableLinking && mode !== "line") { |
||||
|
options.onClick = this.onGraphClicked.bind(this); |
||||
|
} |
||||
|
return options; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Removes the legend tooltip (if any). |
||||
|
*/ |
||||
|
removeLegendTooltip() { |
||||
|
if (this.legendTooltip) { |
||||
|
this.legendTooltip.remove(); |
||||
|
this.legendTooltip = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Removes all existing tooltips (if any). |
||||
|
*/ |
||||
|
removeTooltips() { |
||||
|
if (this.tooltip) { |
||||
|
this.tooltip.remove(); |
||||
|
this.tooltip = null; |
||||
|
} |
||||
|
this.removeLegendTooltip(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Instantiates a Chart (Chart.js lib) to render the graph according to |
||||
|
* the current config. |
||||
|
*/ |
||||
|
renderChart() { |
||||
|
if (this.chart) { |
||||
|
this.chart.destroy(); |
||||
|
} |
||||
|
const config = this.getChartConfig(); |
||||
|
this.chart = new Chart(this.canvasRef.el, config); |
||||
|
// To perform its animations, ChartJS will perform each animation
|
||||
|
// step in the next animation frame. The initial rendering itself
|
||||
|
// is delayed for consistency. We can avoid this by manually
|
||||
|
// advancing the animation service.
|
||||
|
Chart.animationService.advance(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
GraphRenderer.template = "web.GraphRenderer"; |
||||
|
GraphRenderer.props = ["model", "onGraphClicked"]; |
@ -0,0 +1,160 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
import { _lt } from "@web/core/l10n/translation"; |
||||
|
import { registry } from "@web/core/registry"; |
||||
|
import { useService } from "@web/core/utils/hooks"; |
||||
|
import { GroupByMenu } from "@web/search/group_by_menu/group_by_menu"; |
||||
|
import { standardViewProps } from "@web/views/helpers/standard_view_props"; |
||||
|
import { useSetupView } from "@web/views/helpers/view_hook"; |
||||
|
import { Layout } from "@web/views/layout"; |
||||
|
import { useModel } from "@web/views/helpers/model"; |
||||
|
import { GraphArchParser } from "./graph_arch_parser"; |
||||
|
import { GraphModel } from "./graph_model"; |
||||
|
import { GraphRenderer } from "./graph_renderer"; |
||||
|
|
||||
|
const viewRegistry = registry.category("views"); |
||||
|
|
||||
|
const { Component } = owl; |
||||
|
|
||||
|
export class GraphView extends Component { |
||||
|
setup() { |
||||
|
this.actionService = useService("action"); |
||||
|
|
||||
|
let modelParams; |
||||
|
if (this.props.state) { |
||||
|
modelParams = this.props.state.metaData; |
||||
|
} else { |
||||
|
const { arch, fields } = this.props; |
||||
|
const parser = new this.constructor.ArchParser(); |
||||
|
const archInfo = parser.parse(arch, fields); |
||||
|
modelParams = { |
||||
|
additionalMeasures: this.props.additionalMeasures, |
||||
|
disableLinking: Boolean(archInfo.disableLinking), |
||||
|
displayScaleLabels: this.props.displayScaleLabels, |
||||
|
fieldAttrs: archInfo.fieldAttrs, |
||||
|
fields: this.props.fields, |
||||
|
groupBy: archInfo.groupBy, |
||||
|
measure: archInfo.measure || "__count", |
||||
|
mode: archInfo.mode || "bar", |
||||
|
order: archInfo.order || null, |
||||
|
resModel: this.props.resModel, |
||||
|
stacked: "stacked" in archInfo ? archInfo.stacked : true, |
||||
|
title: archInfo.title || this.env._t("Untitled"), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
this.model = useModel(this.constructor.Model, modelParams); |
||||
|
|
||||
|
useSetupView({ |
||||
|
getLocalState: () => { |
||||
|
return { metaData: this.model.metaData }; |
||||
|
}, |
||||
|
getContext: () => this.getContext(), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @returns {Object} |
||||
|
*/ |
||||
|
getContext() { |
||||
|
// expand context object? change keys?
|
||||
|
const { measure, groupBy, mode } = this.model.metaData; |
||||
|
return { |
||||
|
graph_measure: measure, |
||||
|
graph_mode: mode, |
||||
|
graph_groupbys: groupBy.map((gb) => gb.spec), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {string} domain the domain of the clicked area |
||||
|
*/ |
||||
|
onGraphClicked(domain) { |
||||
|
const { context, resModel, title } = this.model.metaData; |
||||
|
|
||||
|
const views = {}; |
||||
|
for (const [viewId, viewType] of this.env.config.views || []) { |
||||
|
views[viewType] = viewId; |
||||
|
} |
||||
|
function getView(viewType) { |
||||
|
return [views[viewType] || false, viewType]; |
||||
|
} |
||||
|
const actionViews = [getView("list"), getView("form")]; |
||||
|
|
||||
|
this.actionService.doAction( |
||||
|
{ |
||||
|
context, |
||||
|
domain, |
||||
|
name: title, |
||||
|
res_model: resModel, |
||||
|
target: "current", |
||||
|
type: "ir.actions.act_window", |
||||
|
views: actionViews, |
||||
|
}, |
||||
|
{ |
||||
|
viewType: "list", |
||||
|
} |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {CustomEvent} ev |
||||
|
*/ |
||||
|
onMeasureSelected(ev) { |
||||
|
const { measure } = ev.detail.payload; |
||||
|
this.model.updateMetaData({ measure }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {"bar"|"line"|"pie"} mode |
||||
|
*/ |
||||
|
onModeSelected(mode) { |
||||
|
this.model.updateMetaData({ mode }); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @param {"ASC"|"DESC"} order |
||||
|
*/ |
||||
|
toggleOrder(order) { |
||||
|
const { order: currentOrder } = this.model.metaData; |
||||
|
const nextOrder = currentOrder === order ? null : order; |
||||
|
this.model.updateMetaData({ order: nextOrder }); |
||||
|
} |
||||
|
|
||||
|
toggleStacked() { |
||||
|
const { stacked } = this.model.metaData; |
||||
|
this.model.updateMetaData({ stacked: !stacked }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
GraphView.template = "web.GraphView"; |
||||
|
GraphView.buttonTemplate = "web.GraphView.Buttons"; |
||||
|
|
||||
|
GraphView.components = { GroupByMenu, Renderer: GraphRenderer, Layout }; |
||||
|
|
||||
|
GraphView.defaultProps = { |
||||
|
additionalMeasures: [], |
||||
|
displayGroupByMenu: false, |
||||
|
displayScaleLabels: true, |
||||
|
}; |
||||
|
|
||||
|
GraphView.props = { |
||||
|
...standardViewProps, |
||||
|
additionalMeasures: { type: Array, elements: String, optional: true }, |
||||
|
displayGroupByMenu: { type: Boolean, optional: true }, |
||||
|
displayScaleLabels: { type: Boolean, optional: true }, |
||||
|
}; |
||||
|
|
||||
|
GraphView.type = "graph"; |
||||
|
|
||||
|
GraphView.display_name = _lt("Graph"); |
||||
|
GraphView.icon = "fa-bar-chart"; |
||||
|
GraphView.multiRecord = true; |
||||
|
|
||||
|
GraphView.Model = GraphModel; |
||||
|
|
||||
|
GraphView.ArchParser = GraphArchParser; |
||||
|
|
||||
|
GraphView.searchMenuTypes = ["filter", "groupBy", "comparison", "favorite"]; |
||||
|
|
||||
|
viewRegistry.add("graph", GraphView); |
@ -0,0 +1,57 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
|
||||
|
import { browser } from "@web/core/browser/browser"; |
||||
|
import { DropdownItem } from "@web/core/dropdown/dropdown_item"; |
||||
|
import { registry } from "@web/core/registry"; |
||||
|
import { useEffect, useService } from "@web/core/utils/hooks"; |
||||
|
|
||||
|
const { Component } = owl; |
||||
|
|
||||
|
const userMenuRegistry = registry.category("user_menuitems"); |
||||
|
|
||||
|
class UserMenuItem extends DropdownItem { |
||||
|
setup() { |
||||
|
super.setup(); |
||||
|
useEffect( |
||||
|
() => { |
||||
|
if (this.props.payload.id) { |
||||
|
this.el.dataset.menu = this.props.payload.id; |
||||
|
} |
||||
|
}, |
||||
|
() => [] |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export class UserMenu extends Component { |
||||
|
setup() { |
||||
|
this.user = useService("user"); |
||||
|
const { origin } = browser.location; |
||||
|
const { userId } = this.user; |
||||
|
this.source = `${origin}/web/image?model=res.users&field=avatar_128&id=${userId}`; |
||||
|
} |
||||
|
|
||||
|
getElements() { |
||||
|
const sortedItems = userMenuRegistry |
||||
|
.getAll() |
||||
|
.map((element) => element(this.env)) |
||||
|
.sort((x, y) => { |
||||
|
const xSeq = x.sequence ? x.sequence : 100; |
||||
|
const ySeq = y.sequence ? y.sequence : 100; |
||||
|
return xSeq - ySeq; |
||||
|
}); |
||||
|
return sortedItems; |
||||
|
} |
||||
|
|
||||
|
onDropdownItemSelected(ev) { |
||||
|
ev.detail.payload.callback(); |
||||
|
} |
||||
|
} |
||||
|
UserMenu.template = "web.UserMenu"; |
||||
|
UserMenu.components = { UserMenuItem }; |
||||
|
|
||||
|
const systrayItem = { |
||||
|
Component: UserMenu, |
||||
|
isDisplayed: (env) => true, |
||||
|
}; |
||||
|
registry.category("systray").add("web.user_menu", systrayItem, { sequence: 0 }); |
@ -0,0 +1,68 @@ |
|||||
|
/* date time picker colour changes for the theme */ |
||||
|
.datepicker { |
||||
|
.table-sm { |
||||
|
> thead { |
||||
|
> tr > .prev { |
||||
|
color: #fff !important; |
||||
|
background-color: $primary_accent !important; |
||||
|
&:hover{ |
||||
|
background-color: darken($primary_accent, 10%) !important; |
||||
|
} |
||||
|
> .fa{ |
||||
|
color: #fff !important; |
||||
|
} |
||||
|
} |
||||
|
> tr > .next { |
||||
|
color: #fff !important; |
||||
|
background-color: $primary_accent !important; |
||||
|
&:hover{ |
||||
|
background-color: darken($primary_accent, 10%) !important; |
||||
|
} |
||||
|
> .fa{ |
||||
|
color: #fff !important; |
||||
|
} |
||||
|
} |
||||
|
> tr > .picker-switch { |
||||
|
color: #fff !important; |
||||
|
background-color: $primary_accent !important; |
||||
|
&:hover{ |
||||
|
background-color: darken($primary_accent, 10%) !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
> tbody > tr > td { |
||||
|
&.today:before { |
||||
|
border-bottom-color: $primary_accent !important; |
||||
|
} |
||||
|
&.active { |
||||
|
background-color: $primary_accent !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.picker-switch { |
||||
|
span.fa { |
||||
|
margin: 0; |
||||
|
@include transition($btn-transition); |
||||
|
&.primary { |
||||
|
background-color: $primary_accent; |
||||
|
color: white; |
||||
|
&:hover { |
||||
|
background-color: darken($primary_accent, 20%); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.daterangepicker .drp-calendar .calendar-table thead tr:first-child { |
||||
|
color: #FFFFFF; |
||||
|
background-color: $primary_accent; |
||||
|
} |
||||
|
|
||||
|
.daterangepicker .drp-calendar .calendar-table tbody tr td:not(.off).active, .daterangepicker .drp-calendar .calendar-table tbody tr td:not(.off).active:hover { |
||||
|
background-color: $primary_accent; |
||||
|
} |
||||
|
|
||||
|
.daterangepicker .drp-calendar .calendar-table thead tr:first-child th.prev:hover, .daterangepicker .drp-calendar .calendar-table thead tr:first-child th.next:hover { |
||||
|
background-color: darken($primary_accent, 20%); |
||||
|
} |
@ -0,0 +1,146 @@ |
|||||
|
#wrapwrap > main { |
||||
|
background: #f8f8fb; |
||||
|
} |
||||
|
.navbar { |
||||
|
background: #fff !important; |
||||
|
} |
||||
|
body { |
||||
|
font-family: 'Poppins', sans-serif !important; |
||||
|
} |
||||
|
body.bg-100 { |
||||
|
background-color: #000000 !important; |
||||
|
} |
||||
|
.card.o_database_list { |
||||
|
align-items: center; |
||||
|
max-width: 450px !important |
||||
|
} |
||||
|
.card.o_database_list .card-body { |
||||
|
background-color: #fff !important; |
||||
|
border-radius: 5px !important; |
||||
|
-webkit-box-shadow: 0 0.75rem 1.5rem rgba(18,38,63,.03) !important; |
||||
|
box-shadow: 0 0.75rem 1.5rem rgba(18,38,63, .03) !important; |
||||
|
width: 450px; |
||||
|
} |
||||
|
|
||||
|
a { |
||||
|
color: #556ee6; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
a:hover { |
||||
|
color: #4458b8; |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
.alert-info { |
||||
|
color: #306391; |
||||
|
background-color: #dcedfc; |
||||
|
border-color: #cbe4fb; |
||||
|
} |
||||
|
.oe_login_form button.btn-link { |
||||
|
color: #495057; |
||||
|
font-weight: 500; |
||||
|
font-size: 14px !important; |
||||
|
} |
||||
|
.oe_login_form button.btn-link:hover { |
||||
|
color: #171a1c; |
||||
|
} |
||||
|
|
||||
|
//login button starts |
||||
|
.btn-primary { |
||||
|
color: #fff; |
||||
|
background-color: #556ee6; |
||||
|
border-color: #556ee6; |
||||
|
} |
||||
|
.btn-primary:hover { |
||||
|
color: #fff; |
||||
|
background-color: #485ec4; |
||||
|
border-color: #4458b8; |
||||
|
} |
||||
|
.btn-check:active+.btn-primary, |
||||
|
.btn-check:checked+.btn-primary, |
||||
|
.btn-primary.active,.btn-primary:active, |
||||
|
.show>.btn-primary.dropdown-toggle { |
||||
|
color: #fff; |
||||
|
background-color: #4458b8 !important; |
||||
|
border-color: #4053ad !important; |
||||
|
} |
||||
|
.btn-check:focus+.btn-primary, .btn-primary:focus { |
||||
|
color: #fff; |
||||
|
background-color: #485ec4 !important; |
||||
|
border-color: #4458b8 !important; |
||||
|
-webkit-box-shadow: 0 0 0 .15rem rgba(111,132,234,.5) !important; |
||||
|
box-shadow: 0 0 0 .15rem rgba(111,132,234,.5) !important; |
||||
|
} |
||||
|
.oe_login_form .btn { |
||||
|
display: inline-block; |
||||
|
cursor: pointer; |
||||
|
-webkit-user-select: none; |
||||
|
-moz-user-select: none; |
||||
|
-ms-user-select: none; |
||||
|
user-select: none; |
||||
|
padding: .47rem .75rem; |
||||
|
border-radius: .25rem; |
||||
|
-webkit-transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; |
||||
|
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; |
||||
|
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; |
||||
|
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; |
||||
|
} |
||||
|
.btn-secondary { |
||||
|
color: #fff !important; |
||||
|
background-color: #74788d !important; |
||||
|
border-color: #74788d !important; |
||||
|
} |
||||
|
.btn-secondary:hover { |
||||
|
color: #fff !important; |
||||
|
background-color: #636678 !important; |
||||
|
border-color: #5d6071 !important; |
||||
|
} |
||||
|
.btn-secondary:active { |
||||
|
color: #fff; |
||||
|
background-color: #5d6071 !important; |
||||
|
border-color: #575a6a !important; |
||||
|
} |
||||
|
.btn-secondary i,.btn-secondary span { |
||||
|
color: #fff !important; |
||||
|
} |
||||
|
.btn-fill-secondary:focus, .btn-secondary:focus, .btn-fill-secondary.focus, .focus.btn-secondary { |
||||
|
box-shadow: none !important; |
||||
|
} |
||||
|
//login button ends |
||||
|
|
||||
|
//input starts |
||||
|
.oe_login_form input { |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
height: 40px !important; |
||||
|
padding: 10px 20px; |
||||
|
font-size: 13px; |
||||
|
font-weight: 400; |
||||
|
line-height: 1.5; |
||||
|
color: #495057; |
||||
|
background-color: #fff; |
||||
|
background-clip: padding-box; |
||||
|
border: 1px solid #ced4da !important; |
||||
|
-webkit-appearance: none; |
||||
|
-moz-appearance: none; |
||||
|
appearance: none; |
||||
|
border-radius: .25rem; |
||||
|
-webkit-transition: border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; |
||||
|
transition: border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; |
||||
|
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out; |
||||
|
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out; |
||||
|
box-shadow: none !important; |
||||
|
margin-bottom:10px !important; |
||||
|
} |
||||
|
form label { |
||||
|
font-weight: 400 !important; |
||||
|
} |
||||
|
.oe_login_form a.btn.btn-secondary { |
||||
|
height: 40px; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
padding: 0.35rem 0.75rem; |
||||
|
} |
||||
|
.oe_login_form a.btn.btn-secondary i.fa.fa-database { |
||||
|
margin-left: 5px; |
||||
|
} |
@ -0,0 +1,347 @@ |
|||||
|
.o_form_statusbar{ |
||||
|
.o_statusbar_buttons{ |
||||
|
.btn{ |
||||
|
margin-right: 30px !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.o_cp_left{ |
||||
|
.btn{ |
||||
|
margin-right: 30px !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.o_calendar_buttons > button > .fa{ |
||||
|
color: #ffffff !important; |
||||
|
} |
||||
|
.o_main_navbar, .btn-primary, .btn-primary:active, .o_searchview_facet_label { |
||||
|
background-color: $primary_accent !important; |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
.o_search_panel_section_icon { |
||||
|
color: $primary_accent !important; |
||||
|
} |
||||
|
.btn-secondary { |
||||
|
border-radius: 0; |
||||
|
border: solid 1px $primary_accent !important; |
||||
|
color: $primary_accent !important; |
||||
|
} |
||||
|
.o_list_view .o_list_table thead { |
||||
|
position: sticky; |
||||
|
top: 0; |
||||
|
} |
||||
|
|
||||
|
.breadcrumb-item > a, .o_menu_item > a { |
||||
|
color: $primary_accent !important; |
||||
|
} |
||||
|
|
||||
|
.fa-trash { |
||||
|
color: #f46a6a !important; |
||||
|
} |
||||
|
|
||||
|
.o_main_navbar > a:hover { |
||||
|
background-color: lighten($primary_accent, 10%) !important; |
||||
|
} |
||||
|
|
||||
|
.o_main_navbar > .o_menu_sections > li > a:hover, .o_main_navbar > .o_menu_systray > li > a:hover, .o_main_navbar > .o_menu_sections > li.show > a, .o_main_navbar > .o_menu_systray > li.show > a { |
||||
|
background-color: lighten($primary_accent, 10%) !important; |
||||
|
} |
||||
|
|
||||
|
.o_main_navbar > .o_menu_apps > li > a:hover, .o_main_navbar > .o_menu_apps > li > a:active { |
||||
|
background-color: lighten($primary_accent, 10%) !important; |
||||
|
} |
||||
|
|
||||
|
.o_main_navbar > .o_menu_apps > .dropdown.show > .dropdown-menu.show { |
||||
|
max-height: 100vh !important; |
||||
|
height: 93vh !important; |
||||
|
} |
||||
|
.o_main_navbar > .o_menu_apps > .dropdown.show > .dropdown-menu.show > a { |
||||
|
//border-bottom: 1px solid lighten($primary_accent, 30%); |
||||
|
} |
||||
|
|
||||
|
.o_mail_discuss_sidebar { |
||||
|
background-color: #1c2833; |
||||
|
} |
||||
|
|
||||
|
.dropdown-toggle:after { |
||||
|
background-color: lighten($primary_accent, 10%) !important; |
||||
|
} |
||||
|
|
||||
|
.o_external_button { |
||||
|
border: none !important; |
||||
|
} |
||||
|
|
||||
|
.o_field_x2many_list_row_add > a { |
||||
|
color: $primary_accent !important; |
||||
|
} |
||||
|
|
||||
|
.nav-item > a { |
||||
|
color: $primary_accent !important; |
||||
|
} |
||||
|
|
||||
|
.o_main_navbar > .o_menu_apps > li > a > i { |
||||
|
color: $inverse_accent !important; |
||||
|
font-size: 16px !important; |
||||
|
} |
||||
|
|
||||
|
.o_form_uri > span { |
||||
|
color: $primary_accent !important; |
||||
|
} |
||||
|
|
||||
|
.o_required_modifier.o_input { |
||||
|
background-color: $inverse_accent !important; |
||||
|
color: $primary_accent !important; |
||||
|
border-left: solid 3px #f46a6a !important; |
||||
|
} |
||||
|
|
||||
|
.o_input { |
||||
|
border: solid 1px $primary_accent !important; |
||||
|
color: $primary_accent !important; |
||||
|
} |
||||
|
|
||||
|
.o-no-caret > i, button[aria-pressed=true] { |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
.o_loading { |
||||
|
background-color: $primary_accent; |
||||
|
} |
||||
|
|
||||
|
.fas { |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
.dashboard_mainbar { |
||||
|
width: 100%; |
||||
|
} |
||||
|
|
||||
|
.a_app_menu_title { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.o_menu_apps > .dropdown.show > .dropdown-menu.show:hover .a_app_menu_title { |
||||
|
display: inline-block; |
||||
|
width: 200px; |
||||
|
} |
||||
|
|
||||
|
.o_required_modifier.o_input, .o_required_modifier.o_input { |
||||
|
background-color: $inverse_accent !important; |
||||
|
color: $primary_accent !important; |
||||
|
border-left: solid 3px #f46a6a !important; |
||||
|
} |
||||
|
.o_required_modifier .o_input, .o_required_modifier .o_input { |
||||
|
background-color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
.dropdown-toggle:after { |
||||
|
background-color: #ffffff00 !important; |
||||
|
} |
||||
|
|
||||
|
.o_required_modifier > .o_input_dropdown > .ui-autocomplete-input { |
||||
|
background-color: $inverse_accent !important; |
||||
|
color: $primary_accent !important; |
||||
|
border-left: solid 3px #f46a6a !important; |
||||
|
} |
||||
|
|
||||
|
.o_datepicker.o_field_date.o_field_widget.o_required_modifier > input { |
||||
|
background-color: $inverse_accent !important; |
||||
|
color: $primary_accent !important; |
||||
|
border-left: solid 3px #f46a6a !important; |
||||
|
} |
||||
|
|
||||
|
.ui-state-active { |
||||
|
background-color: $primary_accent !important; |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
.oe_search_bgnd { |
||||
|
background-color: lighten($primary_accent, 20%) !important; |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
.oe_search_tab { |
||||
|
background-color: $primary_accent !important; |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
.o_horizontal_separator { |
||||
|
color: $primary_accent !important |
||||
|
} |
||||
|
|
||||
|
.o_field_widget.o_field_image .o_form_image_controls { |
||||
|
background-color: $primary_accent !important; |
||||
|
} |
||||
|
|
||||
|
.o_field_widget.o_field_image .o_form_image_controls > button { |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
.dropdown-item.o_app.mt0:hover , .dropdown-item.o_app.mt0:hover > .a_app_menu_title{ |
||||
|
background-color: $primary_accent !important; |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
|
||||
|
// .o_address_country{ |
||||
|
// display: none !important; |
||||
|
// } |
||||
|
div.o_boolean_toggle.custom-control.custom-checkbox > input.custom-control-input:checked + label.custom-control-label::before { |
||||
|
background-color: $primary_accent !important; |
||||
|
} |
||||
|
div.o_boolean_toggle.custom-control.custom-checkbox > input.custom-control-input:checked + label.custom-control-label::before { |
||||
|
background-color: $primary_accent !important; |
||||
|
} |
||||
|
.o_mail_systray_item .o_mail_systray_dropdown .o_mail_systray_dropdown_top .o_filter_button.active { |
||||
|
color: $primary_accent; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
.o_mail_user_status.o_user_online { |
||||
|
color: #fff !important; |
||||
|
} |
||||
|
.o_form_view .o_form_statusbar > .o_statusbar_status > .o_arrow_button.btn-primary.disabled::after { |
||||
|
border-left-color: $primary_accent; |
||||
|
} |
||||
|
.btn-link { |
||||
|
font-weight: 400; |
||||
|
color: $primary_accent !important; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
.o_thread_window_header { |
||||
|
background-color: $primary_accent !important; |
||||
|
} |
||||
|
.o_thread_window_close,.o_thread_window_expand{ |
||||
|
color: $inverse_accent !important; |
||||
|
} |
||||
|
.o_menu_sections, .o_menu_systray, .o_web_client > header{ |
||||
|
background: $primary_accent !important; |
||||
|
} |
||||
|
.fa-building-o{ |
||||
|
color: white !important; |
||||
|
} |
||||
|
.o_button_import, .oe_import_file{ |
||||
|
background: #5aa29f !important; |
||||
|
color: white !important; |
||||
|
border: solid 2px #5aa29f !important; |
||||
|
} |
||||
|
.o_button_import:hover, .oe_import_file:hover,.o_button_import:active, .oe_import_file:active{ |
||||
|
background: white !important; |
||||
|
color: #5aa29f !important; |
||||
|
border: solid 2px #5aa29f !important; |
||||
|
} |
||||
|
.o_form_button_save,.o_form_button_edit{ |
||||
|
background: #7BA94F !important; |
||||
|
color: white !important; |
||||
|
border: solid 2px #7BA94F !important; |
||||
|
} |
||||
|
.o_form_button_save:hover,.o_form_button_edit:hover,.o_form_button_save:active,.o_form_button_edit:active{ |
||||
|
background: white !important; |
||||
|
color: #7BA94F !important; |
||||
|
border: solid 2px #7BA94F !important; |
||||
|
} |
||||
|
.o-kanban-button-new, .o_list_button_add,.o_form_button_create{ |
||||
|
background: #b9408d !important; |
||||
|
color: white !important; |
||||
|
border: solid 2px #b9408d !important; |
||||
|
} |
||||
|
.o-kanban-button-new:hover, .o_list_button_add:hover,.o_form_button_create:hover,.o-kanban-button-new:active, .o_list_button_add:active,.o_form_button_create:active{ |
||||
|
background: white !important; |
||||
|
color: #b9408d !important; |
||||
|
border: solid 2px #b9408d !important; |
||||
|
} |
||||
|
.o_form_button_cancel,.o_import_cancel{ |
||||
|
background: #cf4137 !important; |
||||
|
color: white !important; |
||||
|
border: solid 2px #cf4137 !important; |
||||
|
|
||||
|
} |
||||
|
.o_form_button_cancel:hover,.o_import_cancel:hover,.o_form_button_cancel:active,.o_import_cancel:active{ |
||||
|
background: white !important; |
||||
|
color: #cf4137 !important; |
||||
|
border: solid 2px #cf4137 !important; |
||||
|
} |
||||
|
.report_button{ |
||||
|
border-radius: 0 !important; |
||||
|
border: solid 2px $primary_accent; |
||||
|
background: $primary_accent !important; |
||||
|
} |
||||
|
.report_button:hover,.report_button:active{ |
||||
|
border-radius: 0 !important; |
||||
|
border: solid 2px $primary_accent !important; |
||||
|
color: $primary_accent !important; |
||||
|
background: $inverse_accent !important; |
||||
|
} |
||||
|
.btn-primary{ |
||||
|
border-radius: 0 !important; |
||||
|
} |
||||
|
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link{ |
||||
|
border: none; |
||||
|
border-bottom: solid; |
||||
|
font-weight: bold; |
||||
|
} |
||||
|
.nav-link{ |
||||
|
@include hover-focus { |
||||
|
border: none; |
||||
|
} |
||||
|
} |
||||
|
.o_data_row:has(.custom-control-input:checked){ |
||||
|
background: blue !important; |
||||
|
} |
||||
|
.o_field_one2many{ |
||||
|
.o_list_view{ |
||||
|
.table-responsive{ |
||||
|
max-height:50vh; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
thead{ |
||||
|
position: sticky; |
||||
|
position: -webkit-sticky; |
||||
|
top: 0; |
||||
|
} |
||||
|
.o_list_view .o_list_table tbody{ |
||||
|
position: sticky; |
||||
|
top: 30px; |
||||
|
} |
||||
|
.o_list_view{ |
||||
|
.o_list_table{ |
||||
|
thead{ |
||||
|
z-index:999; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.o_list_view .table-responsive .table{ |
||||
|
width: max-content !important; |
||||
|
min-width: 100%; |
||||
|
|
||||
|
thead |
||||
|
{ |
||||
|
z-index:999; |
||||
|
tr:nth-child(1) th{ |
||||
|
position: sticky; |
||||
|
top: 0; |
||||
|
z-index: 999; |
||||
|
background-color: #eeeeee !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
.o_list_view .o_list_table tbody{ |
||||
|
position:initial !important; |
||||
|
} |
||||
|
.o_list_view .table-responsive .table thead{ |
||||
|
z-index: 1; |
||||
|
} |
||||
|
.o_optional_columns_dropdown_toggle{ |
||||
|
z-index: 999; |
||||
|
} |
||||
|
|
||||
|
.o_progressbar .o_progress .o_progressbar_complete { |
||||
|
background-color: #3d9bbb; |
||||
|
} |
||||
|
.o_cp_left .btn { |
||||
|
margin-right: 10px !important; |
||||
|
} |
||||
|
|
||||
|
.o_main_navbar .o_menu_sections { |
||||
|
flex-wrap: wrap !important; |
||||
|
} |