@ -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; |
|||
} |