Browse Source

July 22: [FIX] Bug Fixed 'odoo_dynamic_dashboard'

pull/331/head
RisvanaCybro 9 months ago
parent
commit
98867a750b
  1. 25
      odoo_dynamic_dashboard/README.rst
  2. 3
      odoo_dynamic_dashboard/__init__.py
  3. 34
      odoo_dynamic_dashboard/__manifest__.py
  4. 41
      odoo_dynamic_dashboard/controllers/odoo_dynamic_dashboard.py
  5. 12
      odoo_dynamic_dashboard/data/dashboard_theme_data.xml
  6. 12
      odoo_dynamic_dashboard/doc/RELEASE_NOTES.md
  7. 2
      odoo_dynamic_dashboard/models/__init__.py
  8. 186
      odoo_dynamic_dashboard/models/dashboard_block.py
  9. 93
      odoo_dynamic_dashboard/models/dashboard_menu.py
  10. 53
      odoo_dynamic_dashboard/models/dashboard_theme.py
  11. 51
      odoo_dynamic_dashboard/models/domain_to_sql.py
  12. 3
      odoo_dynamic_dashboard/security/ir.model.access.csv
  13. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/1.png
  14. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/10.png
  15. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/11.png
  16. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/2.png
  17. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/3.png
  18. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/4.png
  19. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/5.png
  20. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/6.png
  21. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/7.png
  22. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/8.png
  23. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/9.png
  24. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/Tiles.png
  25. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/additems.png
  26. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/blocks.png
  27. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/charts.png
  28. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/darkmode.png
  29. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/dashboard_create.png
  30. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/delete.png
  31. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/editlayout.png
  32. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/email.png
  33. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/email_report.png
  34. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/hero.gif
  35. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/mainpage.png
  36. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/newbar.png
  37. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/newtile.png
  38. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/pdf.png
  39. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/pdf_and_mail.png
  40. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/redirect.png
  41. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/sales_dashboard.png
  42. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/savetypes.png
  43. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/themes.png
  44. BIN
      odoo_dynamic_dashboard/static/description/assets/screenshots/themeselect.png
  45. 1825
      odoo_dynamic_dashboard/static/description/index.html
  46. 4
      odoo_dynamic_dashboard/static/lib/js/interactjs.js
  47. 344
      odoo_dynamic_dashboard/static/src/css/dynamic_dashboard.css
  48. 54
      odoo_dynamic_dashboard/static/src/js/DynamicDashboard.js
  49. 82
      odoo_dynamic_dashboard/static/src/js/DynamicDashboardChart.js
  50. 52
      odoo_dynamic_dashboard/static/src/js/DynamicDashboardTile.js
  51. 391
      odoo_dynamic_dashboard/static/src/js/dynamic_dashboard.js
  52. 215
      odoo_dynamic_dashboard/static/src/js/dynamic_dashboard_chart.js
  53. 89
      odoo_dynamic_dashboard/static/src/js/dynamic_dashboard_tile.js
  54. 208
      odoo_dynamic_dashboard/static/src/scss/dynamic_dashboard.scss
  55. 109
      odoo_dynamic_dashboard/static/src/scss/style.scss
  56. 132
      odoo_dynamic_dashboard/static/src/xml/dynamic_dashboard_template.xml
  57. 46
      odoo_dynamic_dashboard/views/dashboard_menu_view.xml
  58. 66
      odoo_dynamic_dashboard/views/dashboard_menu_views.xml
  59. 56
      odoo_dynamic_dashboard/views/dashboard_theme_views.xml
  60. 13
      odoo_dynamic_dashboard/views/dashboard_view.xml
  61. 14
      odoo_dynamic_dashboard/views/dashboard_views.xml
  62. 58
      odoo_dynamic_dashboard/views/dynamic_block_views.xml
  63. 13
      odoo_dynamic_dashboard/wizard/__init__.py
  64. 75
      odoo_dynamic_dashboard/wizard/dashboard_mail.py
  65. 22
      odoo_dynamic_dashboard/wizard/dashboard_mail_views.xml

25
odoo_dynamic_dashboard/README.rst

@ -3,33 +3,32 @@
:alt: License: AGPL-3
Odoo Dynamic Dashboard
======================
==========================
* Dynamically Arrange the dashboard to get the information that are relevant to your business, department, or a specific process or need.
Configuration
=============
* No additional configurations needed.
- No configuration needed
License
=======
Affero General Public License v3.0 (AGPL v3)
(https://www.gnu.org/licenses/agpl-3.0-standalone.html)
Company
-------
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__
License
-------
Affero General Public License, Version 3 (AGPL-3).
(https://www.gnu.org/licenses/agpl-3.0-standalone.html)
Credits
-------
* Developers : Irfan, Afras,
(V16) Amal Prasad,
(V17) Arjun S,
* Developers: (V17) Arjun S,
(V16) Robin, Afra MP
* Contact: odoo@cybrosys.com
Contacts
--------
* Mail Contact : odoo@cybrosys.com
* Website : https://cybrosys.com
* Website : http://www.cybrosys.com
Bug Tracker
-----------
@ -39,10 +38,8 @@ Maintainer
==========
.. image:: https://cybrosys.com/images/logo.png
:target: https://cybrosys.com
This module is maintained by Cybrosys Technologies.
For support and more information, please visit `Our Website <https://cybrosys.com/>`__
For support and more information, please visit https://www.cybrosys.com
Further information
===================

3
odoo_dynamic_dashboard/__init__.py

@ -19,5 +19,6 @@
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from . import models
from . import controllers
from . import models
from . import wizard

34
odoo_dynamic_dashboard/__manifest__.py

@ -21,34 +21,42 @@
#############################################################################
{
'name': "Odoo Dynamic Dashboard",
'version': '17.0.1.0.0',
'category': 'Extra Tools ',
'summary': """Odoo Dynamic Dashboard, Dynamic Dashboard, Odoo Dashboard, Dynamic Dashbaord, AI Dashboard, Odoo17 Dashboard, Dashboard, Odoo17, Configurable Dashboard""",
'description': """Create Configurable Dashboard Dynamically to get the
information that are relevant to your business, department,
or a specific process or need, Dynamic Dashboard, Dashboard ,
Dashboard Odoo""",
'version': '17.0.2.0.0',
'category': 'Productivity',
'summary': """Create Configurable Dashboards Easily""",
'description': """Create Configurable Odoo Dynamic Dashboard to get the
information that are relevant to your business, department, or a specific
process or need""",
'author': 'Cybrosys Techno Solutions',
'company': 'Cybrosys Techno Solutions',
'maintainer': 'Cybrosys Techno Solutions',
'website': 'https://www.cybrosys.com',
'website': "https://www.cybrosys.com",
'depends': ['web'],
'data': [
'security/ir.model.access.csv',
'views/dashboard_view.xml',
'views/dashboard_menu_view.xml',
'views/dynamic_block_view.xml',
'data/dashboard_theme_data.xml',
'views/dashboard_views.xml',
'views/dynamic_block_views.xml',
'views/dashboard_menu_views.xml',
'views/dashboard_theme_views.xml',
'wizard/dashboard_mail_views.xml',
],
'assets': {
'web.assets_backend': [
'odoo_dynamic_dashboard/static/src/js/**/*.js',
'odoo_dynamic_dashboard/static/src/css/**/*.css',
'odoo_dynamic_dashboard/static/src/scss/**/*.scss',
'odoo_dynamic_dashboard/static/src/js/**/*.js',
'odoo_dynamic_dashboard/static/src/xml/**/*.xml',
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css',
'odoo_dynamic_dashboard/static/lib/js/interactjs.js',
],
},
'images': ['static/description/banner.png'],
'images': ['static/description/banner.jpg'],
'license': "AGPL-3",
'installable': True,
'auto_install': False,
'application': True,
}

41
odoo_dynamic_dashboard/controllers/odoo_dynamic_dashboard.py

@ -24,38 +24,11 @@ from odoo.http import request
class DynamicDashboard(http.Controller):
"""
This is the class DynamicDashboard which is the subclass of the class
http.Controller
"""
"""Class to search and filter values in dashboard"""
@http.route('/create/tile', type='json', auth='user')
def tile_creation(self, **kw):
"""This is the method to create the tile when create on the button
ADD BLOCK"""
tile_type = kw.get('type')
action_id = kw.get('action_id')
request.env['dashboard.block'].get_dashboard_vals(action_id)
tile_id = request.env['dashboard.block'].sudo().create({
'name': 'New Block',
'type': tile_type,
'tile_color': '#1f6abb',
'text_color': '#FFFFFF',
'fa_icon': 'fa fa-money',
'fa_color': '#132e45',
'edit_mode': True,
'client_action': int(action_id),
})
return {'id': tile_id.id, 'name': tile_id.name, 'type': tile_type, 'icon': 'fa fa-money',
'color': '#1f6abb',
'tile_color': '#1f6abb',
'text_color': '#FFFFFF',
'icon_color': '#1f6abb'}
@http.route('/get/values', type='json', auth='user')
def get_value(self, **kw):
"""This is the method get_value which will get the records inside the
tile"""
action_id = kw.get('action_id')
datas = request.env['dashboard.block'].get_dashboard_vals(action_id)
return datas
@http.route('/custom_dashboard/search_input_chart', type='json',
auth="public", website=True)
def dashboard_search_input_chart(self, search_input):
"""Function to filter search input in dashboard"""
return request.env['dashboard.block'].search([
('name', 'ilike', search_input)]).ids

12
odoo_dynamic_dashboard/data/dashboard_theme_data.xml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<!-- Demo Dashboard Theme -->
<record id="demo_theme" model="dashboard.theme">
<field name="name">Demo</field>
<field name="color_x">#4158D0</field>
<field name="color_y">#C850C0</field>
<field name="color_z">#FFCC70</field>
</record>
</data>
</odoo>

12
odoo_dynamic_dashboard/doc/RELEASE_NOTES.md

@ -1,7 +1,13 @@
## Module <odoo_dynamic_dashboard>
#### 02.03.2024
#### 18.05.2024
#### Version 17.0.1.0.0
#### ADD
##### ADD
- Initial commit for Odoo Dynamic Dashboard
- Initial Commit for Odoo Dynamic Dashboard
## Module <odoo_dynamic_dashboard>
#### 20.07.2024
#### Version 17.0.2.0.0
##### UPDT
- New Features has been Added.

2
odoo_dynamic_dashboard/models/__init__.py

@ -20,6 +20,6 @@
#
#############################################################################
from . import dashboard_block
from . import dashboard_block_line
from . import dashboard_menu
from . import dashboard_theme
from . import domain_to_sql

186
odoo_dynamic_dashboard/models/dashboard_block.py

@ -19,140 +19,164 @@
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import models, fields, _
from odoo.exceptions import ValidationError
from odoo.osv import expression
from ast import literal_eval
from odoo import api, fields, models
from odoo.osv import expression
class DashboardBlock(models.Model):
"""Creates the model Dashboard Blocks"""
"""Class is used to create charts and tiles in dashboard"""
_name = "dashboard.block"
_description = "Dashboard Blocks"
_description = "Dashboard Block"
def get_default_action(self):
"""This is the method get_default_action which will return the default
action id."""
"""Function to get values from dashboard if action_id is true return
id else return false"""
action_id = self.env.ref(
'odoo_dynamic_dashboard.dynamic_dashboard_action')
'odoo_dynamic_dashboard.dashboard_view_action')
if action_id:
return action_id.id
return False
name = fields.Char(string="Name", help='Name of the block')
field_id = fields.Many2one('ir.model.fields', string='Measured Field',
domain="[('store', '=', True), ('model_id', '=', model_id), ('ttype', 'in', ['float','integer','monetary'])]",
help='Measured field for the block')
fa_icon = fields.Char(string="Icon", help='Icon for the block')
graph_size = fields.Selection(
selection=[("col-lg-4", "Small"), ("col-lg-6", "Medium"),
("col-lg-12", "Large")],
string="Graph Size", default='col-lg-4', help="Size of the graph")
fa_icon = fields.Char(string="Icon", help="Add icon for tile")
operation = fields.Selection(
selection=[("sum", "Sum"), ("avg", "Average"), ("count", "Count")],
string="Operation",
help='Tile Operation that needs to bring values for tile')
help='Tile Operation that needs to bring values for tile',
required=True)
graph_type = fields.Selection(
selection=[("bar", "Bar"), ("radar", "Radar"), ("pie", "Pie"),
("line", "Line"), ("doughnut", "Doughnut")],
("polarArea", "polarArea"), ("line", "Line"),
("doughnut", "Doughnut")],
string="Chart Type", help='Type of Chart')
measured_field = fields.Many2one("ir.model.fields", string="Measured Field",
help='Measure field for the chart')
client_action = fields.Many2one('ir.actions.client',
default=get_default_action,
string="Client Action",
help='Client Action for the dashboard '
'block')
measured_field_id = fields.Many2one("ir.model.fields",
string="Measured Field",
help="Select the Measured")
client_action_id = fields.Many2one('ir.actions.client',
string="Client action",
default=get_default_action,
help="Client action")
type = fields.Selection(
selection=[("graph", "Chart"), ("tile", "Tile")], string="Type",
help='Type of Block ie, Chart or Tile')
x_axis = fields.Char(string="X-Axis", help="X-axis for the chart")
y_axis = fields.Char(string="Y-Axis", help="Y-axis for the chart")
group_by = fields.Many2one("ir.model.fields", store=True,
string="Group by(Y-Axis)",
help='Field value for Y-Axis',
domain="[('store', '=', True)]")
selection=[("graph", "Chart"), ("tile", "Tile")],
string="Type", help='Type of Block ie, Chart or Tile')
x_axis = fields.Char(string="X-Axis", help="Chart X-axis")
y_axis = fields.Char(string="Y-Axis", help="Chart Y-axis")
height = fields.Char(string="Height ", help="Height of the block")
width = fields.Char(string="Width", help="Width of the block")
translate_x = fields.Char(string="Translate_X",
help="x value for the style transform translate")
translate_y = fields.Char(string="Translate_Y",
help="y value for the style transform translate")
data_x = fields.Char(string="Data_X", help="Data x value for resize")
data_y = fields.Char(string="Data_Y", help="Data y value for resize")
group_by_id = fields.Many2one("ir.model.fields", store=True,
string="Group by(Y-Axis)",
help='Field value for Y-Axis')
tile_color = fields.Char(string="Tile Color", help='Primary Color of Tile')
text_color = fields.Char(string="Text Color", help='Text Color of Tile')
val_color = fields.Char(string="Value Color", help='Value Color of Tile')
fa_color = fields.Char(string="Icon Color", help='Icon Color of Tile')
filter = fields.Char(string="Filter", help='Filter for Tile')
filter = fields.Char(string="Filter", help="Add filter")
model_id = fields.Many2one('ir.model', string='Model',
help='Model for Tile')
model_name = fields.Char(related='model_id.model', readonly=True,
string="Model Name", help='Model Name of Tile')
filter_by = fields.Many2one("ir.model.fields", string=" Filter By",
help="Filter By for Tile")
filter_values = fields.Char(string="Filter Values",
help="Filter Values for tiles accordingly")
sequence = fields.Integer(string="Sequence",
help="sequence of the dashboard")
edit_mode = fields.Boolean(default=False, invisible=True,
string="Edit Mode", help="Edit mode of the tile")
help="Select the module name")
model_name = fields.Char(related='model_id.model', string="Model Name",
help="Added model_id model")
edit_mode = fields.Boolean(string="Edit Mode",
help="Enable to edit chart and tile",
default=False, invisible=True)
def get_dashboard_vals(self, action_id):
"""Dashboard block values"""
@api.onchange('model_id')
def _onchange_model_id(self):
if self.operation or self.measured_field_id:
self.operation = False
self.measured_field_id = False
def get_dashboard_vals(self, action_id, start_date=None, end_date=None):
"""Fetch block values from js and create chart"""
block_id = []
for rec in self.env['dashboard.block'].sudo().search(
[('client_action', '=', int(action_id))]):
[('client_action_id', '=', int(action_id))]):
if rec.filter is False:
rec.filter = "[]"
filter_list = literal_eval(rec.filter)
filter_list = [filter_item for filter_item in filter_list if not (
isinstance(filter_item, tuple) and filter_item[
0] == 'create_date')]
vals = {
'id': rec.id,
'name': rec.name,
'type': rec.type,
'graph_type': rec.graph_type,
'icon': rec.fa_icon,
'cols': rec.graph_size,
'color': rec.tile_color if rec.tile_color else '#1f6abb;',
'text_color': rec.text_color if rec.text_color else '#FFFFFF;',
'icon_color': rec.fa_color if rec.fa_color else '#1f6abb;',
'tile_color': rec.tile_color if rec.tile_color else '#FFFFFF;',
'model_name': rec.model_name,
'measured_field': rec.measured_field.field_description if rec.measured_field else None,
'y_field': rec.measured_field.name,
'x_field': rec.group_by.name,
'operation': rec.operation,
'domain': filter_list
}
rec.filter = repr(filter_list)
vals = {'id': rec.id, 'name': rec.name, 'type': rec.type,
'graph_type': rec.graph_type, 'icon': rec.fa_icon,
'model_name': rec.model_name,
'color': f'background-color: {rec.tile_color};' if rec.tile_color else '#1f6abb;',
'text_color': f'color: {rec.text_color};' if rec.text_color else '#FFFFFF;',
'val_color': f'color: {rec.val_color};' if rec.val_color else '#FFFFFF;',
'icon_color': f'color: {rec.tile_color};' if rec.tile_color else '#1f6abb;',
'height': rec.height,
'width': rec.width,
'translate_x': rec.translate_x,
'translate_y': rec.translate_y,
'data_x': rec.data_x,
'data_y': rec.data_y,
'domain': filter_list,
}
domain = []
if rec.filter:
domain = expression.AND([literal_eval(rec.filter)])
if rec.model_name:
if rec.type == 'graph':
query = self.env[rec.model_name].get_query(domain,
rec.operation,
rec.measured_field,
group_by=rec.group_by)
try:
self._cr.execute(query)
except Exception as exc:
raise ValidationError(
_(f"Could'nt fetch data try another group by field for {rec.name} block")) from exc
self._cr.execute(self.env[rec.model_name].get_query(domain,
rec.operation,
rec.measured_field_id,
start_date,
end_date,
group_by=rec.group_by_id))
records = self._cr.dictfetchall()
x_axis = []
for record in records:
x_axis.append(record.get(rec.group_by.name))
if record.get('name') and type(
record.get('name')) == dict:
x_axis.append(record.get('name')[self._context.get(
'lang') or 'en_US'])
else:
x_axis.append(record.get(rec.group_by_id.name))
y_axis = []
for record in records:
y_axis.append(record.get('value'))
vals.update({'x_axis': x_axis, 'y_axis': y_axis})
else:
query = self.env[rec.model_name].get_query(domain,
rec.operation,
rec.measured_field)
self._cr.execute(query)
self._cr.execute(self.env[rec.model_name].get_query(domain,
rec.operation,
rec.measured_field_id,
start_date,
end_date))
records = self._cr.dictfetchall()
magnitude = 0
total = records[0].get('value')
while abs(total) >= 1000:
magnitude += 1
total /= 1000.0
val = f'{total:.2f}{" KMGTP"[magnitude]}' if magnitude else f'{total:.2f}'
val = '%.2f%s' % (
total, ['', 'K', 'M', 'G', 'T', 'P'][magnitude])
records[0]['value'] = val
vals.update(records[0])
block_id.append(vals)
return block_id
def get_save_layout(self, grid_data_list):
"""Function fetch edited values while edit layout of the chart or tile
and save values in a database"""
for data in grid_data_list:
block = self.browse(int(data['id']))
if data.get('data-x'):
block.write({
'translate_x': f"{data['data-x']}px",
'translate_y': f"{data['data-y']}px",
'data_x': data['data-x'],
'data_y': data['data-y'],
})
if data.get('height'):
block.write({
'height': f"{data['height']}px",
'width': f"{data['width']}px",
})
return True

93
odoo_dynamic_dashboard/models/dashboard_menu.py

@ -23,77 +23,58 @@ from odoo import api, fields, models
class DashboardMenu(models.Model):
"""
This is the class DashboardMenu which is the subclass of the class Model
which is here used to create the model dashboard.menu.
"""
"""Class to create new dashboard menu"""
_name = "dashboard.menu"
_description = "Dashboard Menu"
name = fields.Char(string="Name", help="Name of the dashboard")
parent_id = fields.Many2one('ir.ui.menu', string="Menu",
help="Parent of the dashboard")
name = fields.Char(string="Name", ondelete='cascade',
help="Enter a name for the dashboard menu")
menu_id = fields.Many2one('ir.ui.menu', string="Parent Menu",
help="Parent Menu Location of New Dashboard",
ondelete='cascade')
group_ids = fields.Many2many('res.groups', string='Groups',
related='parent_id.groups_id',
help="User need to be at least in one "
"of these groups to see the menu")
related='menu_id.groups_id',
help="User need to be at least in one of "
"these groups to see the menu")
client_action_id = fields.Many2one('ir.actions.client',
string='Client Action',
help="Client Action Related "
"to the dashboard")
menu_id = fields.Many2one('ir.ui.menu', string="Created Menu",
help="Created menu")
string="Client Action",
help="Client action of the "
"corresponding dashboard menu")
@api.model
def create(self, vals):
"""
Summary:
This is the create function of the model DashboardMenu which is
triggered when creating a new record in this model.
Args:
vals:
The values when the user creating a new record.
Returns:
res:
Returns the created record at the end
"""
values = {
"""Function to create new dashboard menu"""
action_id = self.env['ir.actions.client'].create({
'name': vals['name'],
'tag': 'owl.dynamic_dashboard',
}
action_id = self.env['ir.actions.client'].create(values)
'tag': 'OdooDynamicDashboard',
})
vals['client_action_id'] = action_id.id
menu_id = self.env['ir.ui.menu'].create({
self.env['ir.ui.menu'].create({
'name': vals['name'],
'parent_id': vals['parent_id'],
'action': f'ir.actions.client,{action_id.id}'
'parent_id': vals['menu_id'],
'action': 'ir.actions.client,%d' % (action_id.id,)
})
res = super(DashboardMenu, self).create(vals)
res.menu_id = menu_id.id
return res
return super(DashboardMenu, self).create(vals)
def write(self, vals):
"""
Summary:
This is the write function of the model DashboardMenu which is
triggered when changing any value in the corresponding record.
Args:
vals:
The values when the user creating a editing an record.
Returns:
Returns the updated record at the end
"""
if self.menu_id:
self.menu_id.update(vals)
"""Function to save edited data in dashboard menu"""
for rec in self:
client_act_id = rec['client_action_id'].id
self.env['ir.ui.menu'].search(
[('parent_id', '=', rec['menu_id'].id),
('action', '=', f'ir.actions.client,{client_act_id}')]).write({
'name': vals['name'] if 'name' in vals.keys() else rec['name'],
'parent_id': vals['menu_id'] if 'menu_id' in vals.keys() else
rec['menu_id'],
'action': f'ir.actions.client,{client_act_id}'
})
return super(DashboardMenu, self).write(vals)
def unlink(self):
"""
Summary:
This is the unlink function of the model DashboardMenu which is
triggered when unlinking any record in this model.
Returns:
Returns the record delete.
"""
self.menu_id.unlink()
"""Delete dashboard along with menu item"""
for rec in self:
self.env['ir.ui.menu'].search(
[('parent_id', '=', rec['menu_id'].id),
('action', '=',
f'ir.actions.client,{rec["client_action_id"].id}')]).unlink()
return super(DashboardMenu, self).unlink()

53
odoo_dynamic_dashboard/models/dashboard_theme.py

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-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 AFFERO
# GENERAL PUBLIC LICENSE (AGPL 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 AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import api, fields, models
class DashboardTheme(models.Model):
_name = 'dashboard.theme'
_description = 'Dashboard Theme'
name = fields.Char(string='Theme Name', help='Name of the theme')
color_x = fields.Char(string='Color X', help='Select the color_x for theme',
default='#4158D0')
color_y = fields.Char(string='Color Y', help='Select the color_y for theme',
default='#C850C0')
color_z = fields.Char(string='Color Z', help='Select the color_z for theme',
default='#FFCC70')
body = fields.Html(string='Body', help='Preview of the theme will be shown')
style = fields.Char(string='Style',
help='It store the style of the gradient')
@api.constrains('name', 'color_x', 'color_y', 'color_z')
def save_record(self):
"""
Function for saving the datas including body and style
"""
self.body = f"<div style='width:300px; height:300px;background-image: linear-gradient(50deg, {self.color_x} 0%, {self.color_y} 46%, {self.color_z} 100%);'/>"
self.style = f"background-image: linear-gradient(50deg, {self.color_x} 0%, {self.color_y} 46%, {self.color_z} 100%);"
def get_records(self):
"""
Function for returning all records with fields name and style
"""
records = self.search_read([], ['name', 'style'])
return records

51
odoo_dynamic_dashboard/models/domain_to_sql.py

@ -22,46 +22,53 @@
from odoo import models
def get_query(self, args, operation, field, group_by=False, apply_ir_rules=False):
"""Dashboard block Query Creation"""
def get_query(self, args, operation, field, start_date=None, end_date=None,
group_by=False, apply_ir_rules=False):
""" Dashboard block Query Creation """
query = self._where_calc(args)
if apply_ir_rules:
self._apply_ir_rules(query, 'read')
if operation and field:
data = 'COALESCE(%s("%s".%s),0) AS value' % (operation.upper(), self._table, field.name)
data = 'COALESCE(%s("%s".%s),0) AS value' % (
operation.upper(), self._table, field.name)
join = ''
group_by_str = ''
if group_by:
if group_by.ttype == 'many2one':
relation_model = group_by.relation.replace('.', '_')
join = ' INNER JOIN %s on "%s".id = "%s".%s' % (
relation_model, relation_model, self._table, group_by.name)
relation_model, relation_model, self._table, group_by.name)
rec_name = self.env[group_by.relation]._rec_name_fallback()
data = data + ',"%s".%s AS %s' % (relation_model, rec_name, group_by.name)
data = data + ',"%s".%s AS %s' % (
relation_model, rec_name, group_by.name)
group_by_str = ' Group by "%s".%s' % (relation_model, rec_name)
else:
data = data + ',"%s".%s' % (self._table, group_by.name)
group_by_str = ' Group by "%s".%s' % (self._table, str(group_by.name))
group_by_str = ' Group by "%s".%s' % (
self._table, str(group_by.name))
else:
data = '"%s".id' % (self._table)
from_clause, where_clause, where_clause_params = query.get_sql()
where_str = where_clause and (" WHERE %s" % where_clause) or ''
if 'company_id' in self._fields:
if len(self.env.companies.ids) > 1:
operator = 'in'
company = str(tuple(self.env.companies.ids))
else:
operator = '='
company = self.env.companies.ids[0]
if where_str == '':
add = ' where'
else:
add = ' and'
multicompany_condition = '%s "%s".company_id %s %s' % (add, self._table, operator, company)
if start_date and start_date != 'null':
start_date_query = f' AND ({from_clause}."create_date" >= \'{start_date}\')'
else:
start_date_query = ''
if end_date and end_date != 'null':
end_date_query = f' AND ({from_clause}."create_date" <= \'{end_date}\')'
else:
multicompany_condition = ''
end_date_query = ''
query_str = 'SELECT %s FROM ' % data + from_clause + join + where_str + start_date_query + end_date_query + group_by_str
def format_param(x):
if not isinstance(x, tuple):
return "'" + str(x) + "'"
elif isinstance(x, tuple) and len(x) == 1:
return "(" + str(x[0]) + ")"
else:
return str(x)
exact_query = query_str % tuple(map(format_param, where_clause_params))
return exact_query
query_str = 'SELECT %s FROM ' % data + from_clause + join + where_str + multicompany_condition + group_by_str
where_clause_params = map(lambda x: "'" + str(x) + "'", where_clause_params)
return query_str % tuple(where_clause_params)
models.BaseModel.get_query = get_query

3
odoo_dynamic_dashboard/security/ir.model.access.csv

@ -1,4 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_dashboard_block,access.dashboard.block,model_dashboard_block,base.group_user,1,1,1,1
access_dashboard_menu,access.dashboard.menu,model_dashboard_menu,base.group_user,1,1,1,1
access_dashboard_block_line,access.dashboard.block.line,model_dashboard_block_line,base.group_user,1,1,1,1
access_dashboard_theme,access.dashboard.theme,model_dashboard_theme,base.group_user,1,1,1,1
access_dashboard_mail,access.dashboard.mail,model_dashboard_mail,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_dashboard_block access.dashboard.block model_dashboard_block base.group_user 1 1 1 1
3 access_dashboard_menu access.dashboard.menu model_dashboard_menu base.group_user 1 1 1 1
4 access_dashboard_block_line access_dashboard_theme access.dashboard.block.line access.dashboard.theme model_dashboard_block_line model_dashboard_theme base.group_user 1 1 1 1
5 access_dashboard_mail access.dashboard.mail model_dashboard_mail base.group_user 1 1 1 1

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/1.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/10.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/11.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/2.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/3.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/4.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/5.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/6.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/7.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/8.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/9.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/Tiles.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/additems.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/blocks.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/charts.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/darkmode.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/dashboard_create.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/delete.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/editlayout.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/email.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/email_report.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/hero.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 370 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/mainpage.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/newbar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/newtile.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/pdf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/pdf_and_mail.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/redirect.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/sales_dashboard.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/savetypes.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/themes.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
odoo_dynamic_dashboard/static/description/assets/screenshots/themeselect.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

1825
odoo_dynamic_dashboard/static/description/index.html

File diff suppressed because it is too large

4
odoo_dynamic_dashboard/static/lib/js/interactjs.js

File diff suppressed because one or more lines are too long

344
odoo_dynamic_dashboard/static/src/css/dynamic_dashboard.css

@ -0,0 +1,344 @@
.row {
margin: 1rem;
}
.resize-drag {
border-radius: 8px;
padding: 20px;
margin: 1rem;
color: white;
font-size: 20px;
font-family: sans-serif;
touch-action: none;
box-sizing: border-box;
}
.theme {
position: absolute;
right: 60.5em;
padding-top:inherit;
}
.card {
background-color: transparent !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 5px !important;
cursor: pointer;
transition: transform 0.2s;
}
.dashboard_pdf{
position: absolute;
right: 26.5em;
padding-top:inherit;
}
.dashboard_mail{
position: absolute;
right: 24.5em;
padding-top:inherit;
}
.resize-drag {
position: relative !important;
font-size: 100%;
overflow:hidden;
}
h2, h3 {
font-size: 150% !important;
}
.container {
max-width: 100% !important;
}
.tile {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 5px;
cursor: pointer;
transition: transform 0.2s;
}
div.card-header {
color: #383838;
background-color: #70659647 !important;
}
/* The toggle-btn - the box around the slider */
.layout-switch {
font-size: 15px;
position: absolute;
left: 10.5em;
display: inline-block;
padding: 0 0 0 0;
border-radius: 0.5rem;
}
.modal-header .btn-close {
display: none;
}
.modal-title{
margin-inline-end: auto;
}
.theme-text {
font-family: Arial, sans-serif;
font-size: 1em;
opacity: 70%;
}
.search-group {
position: absolute;
right: 1.7em;
}
.block_setting {
position: absolute;
top: 7px;
left: 10px;
}
.block_delete {
position: absolute;
top: 7px;
right: 10px;
}
.tile_edit {
color: #d2d2d2;
}
.block_edit {
color: #3f3f3f;
}
.block_setting, .block_delete, .block_image, .block_pdf, .block_xlsx, .block_csv, .block_export {
display: none;
}
.resize-drag:hover .block_setting,
.resize-drag:hover .block_xlsx,
.resize-drag:hover .block_csv,
.resize-drag:hover .block_image,
.resize-drag:hover .block_pdf,
.resize-drag:hover .block_export,
.resize-drag:hover .block_delete {
display: block;
}
.chart-edit {
position: absolute;
top: 0px;
left: 3px;
font-size: 16px;
}
.chart-image {
position: absolute;
top: 3px;
right: 106px;
font-size: 16px;
}
.chart-pdf {
position: absolute;
top: 3px;
right: 82px;
font-size: 16px;
}
.chart-csv {
position: absolute;
top: 3px;
right: 58px;
font-size: 16px;
}
.chart-xlsx {
position: absolute;
top: 6px;
right: 34px;
font-size: 15px;
}
.chart-setting {
position: absolute;
top: 0px;
right: 3px;
font-size: 16px;
}
.chart_title {
padding-top: 0.7em;
text-align: center;
font-size: 16px;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
.move_slider {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
/* Rounded sliders */
.slider.round {
border-radius: 34px;
margin: -1px 6px 11px 0;
}
.slider.round:before {
border-radius: 50%;
}
.o_kanban_record {
background-color: #e8e8e8 !important;
}
.oe_module_name {
background-color: #ffffff !important;
}
.bootbox .modal-footer .btn-danger:hover {
background-color: #ff595c;
}
.bootbox .modal-footer .btn-danger {
box-shadow: none;
margin-left: 0.5rem;
}
.bootbox .modal-header {
text-shadow: -1px 3px 5px #b4b4b4;
}
.bootbox .modal-body {
font-size: 14px;
text-shadow: -1px 3px 5px #b4b4b4;
color: #000000;
}
.dropdown-addblock, .o-dropdown-menu {
left: 2.5em;
top: 2.5em;
}
.navbar {
top:10px;
padding: 1.2rem 0 2.6rem 0 !important;
border-bottom: 1px solid #3f3f3f1a !important;
color: #444444;
background-color: #ffffff !important;
}
.theme_icon:hover {
text-shadow: 0px 0px 5px #9388ff;
}
.navbar-style {
margin-top: 1.2em;
border-radius: 0.5em;
}
.dropdown-add-items {
position: absolute;
color: #e4e4e4;
left: 2em;
font-size: 16px;
text-transform: uppercase;
background-color: #71639e;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1.08333333rem;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.dropdown-add-items:hover {
color: white;
background-color: #59507b;
}
@media (max-width: 767px) {
.navbar {
padding: 1.2rem 0 1.2rem 0;
}
.navbar:focus, .navbar:active {
padding: 1.2rem 0 2rem 0;
}
.o_web_client.o_touch_device .btn, .o_web_client.o_touch_device .btn .btn-sm, .o_web_client.o_touch_device .btn .btn-group-sm > .btn {
font-size: 1.08333333rem;
padding: 6px 8px;
margin-top: 39px;
margin-left: -39px;
}
.dropdown-item {
font-size: 1.08333333rem;
font-weight: 500;
}
.dropdown-addblock, .o-dropdown-menu {
left: -3.5em;
padding: 0em 7em;
margin-top: 38px;
width: 276px;
}
.toggle-btn {
position: absolute;
margin-top: -16px;
margin-left: 56px;
left: 1.2em;
top: 2.3em;
height: 10px;
}
#edit_layout {
display: none;
border-radius: inherit;
}
#search-button {
position: relative;
left: 3.5em;
}
#searchclear {
position: absolute;
margin: 0.3rem 0 0 9.9rem;
}
.search-box {
top: -3em;
left: 7.5em;
margin: 0em -1.9em;
width: 79%;
}
.navbar-toggler {
margin: 0.2em 1em;
padding: 0.25rem 0.5rem;
}
.dropdown-add-items {
position: absolute;
color: #e4e4e4;
top: -2.6em;
left: 3.5em;
}
}

54
odoo_dynamic_dashboard/static/src/js/DynamicDashboard.js

@ -1,54 +0,0 @@
/** @odoo-module */
import { registry} from '@web/core/registry';
import { DynamicDashboardTile} from './DynamicDashboardTile'
import { DynamicDashboardChart} from './DynamicDashboardChart'
import { useService } from "@web/core/utils/hooks";
const { Component, mount} = owl
export class DynamicDashboard extends Component {
setup(){
this.action = useService("action");
this.rpc = this.env.services.rpc
this.renderDashboard()
}
async renderDashboard() {
const action = this.action
const rpc = this.rpc
await this.rpc('/get/values', {'action_id': this.props.actionId}).then(function(response){
if ($('.o_dynamic_dashboard')[0]){
for (let i = 0; i < response.length; i++) {
if (response[i].type === 'tile'){
mount(DynamicDashboardTile, $('.o_dynamic_tile')[0], { props: {
widget: response[i], doAction: action
}});
}
else{
mount(DynamicDashboardChart, $('.o_dynamic_graph')[0], { props: {
widget: response[i], doAction: action, rpc: rpc
}});
}
}
}
})
}
async _onClick_add_block(e){
var self = this;
var self_props = this.props;
var self_env = self.env;
var type = $(e.target).attr('data-type');
await this.rpc('/create/tile',{'type' : type, 'action_id': self.props.actionId}).then(function(response){
if(response['type'] == 'tile'){
mount(DynamicDashboardTile, $('.o_dynamic_tile')[0], { props: {
widget: response, doAction: self.action
}});
}
else{
mount(DynamicDashboardChart, $('.o_dynamic_graph')[0], { props: {
widget: response, doAction: self.action
}});
}
})
}
}
DynamicDashboard.template = "owl.dynamic_dashboard"
registry.category("actions").add("owl.dynamic_dashboard", DynamicDashboard)

82
odoo_dynamic_dashboard/static/src/js/DynamicDashboardChart.js

@ -1,82 +0,0 @@
/** @odoo-module */
import { registry} from '@web/core/registry';
import { loadJS} from '@web/core/assets';
import { getColor } from "@web/core/colors/colors";
const { Component, xml, onWillStart, useRef, onMounted } = owl
export class DynamicDashboardChart extends Component {
setup() {
this.doAction = this.props.doAction.doAction
this.chartRef = useRef("chart")
onWillStart(async () => {
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js")
})
onMounted(()=> this.renderChart())
}
renderChart(){
if (this.props.widget.graph_type){
const x_axis = this.props.widget.x_axis
const y_axis = this.props.widget.y_axis
const data = []
for (let i = 0; i < x_axis.length; i++) {
const value = { key: x_axis[i], value: y_axis[i] }
data.push(value);
}
new Chart(
this.chartRef.el,
{
type: this.props.widget.graph_type || 'bar',
data: {
labels: data.map(row => row.key),
datasets: [
{
label: this.props.widget.measured_field,
data: data.map(row => row.value),
backgroundColor: data.map((_, index) => getColor(index)),
hoverOffset : 4
}
]
},
}
);
}
}
async getConfiguration(){
var id = this.props.widget.id
await this.doAction({
type: 'ir.actions.act_window',
res_model: 'dashboard.block',
res_id: id,
view_mode: 'form',
views: [[false, "form"]]
});
}
}
DynamicDashboardChart.template = xml `
<div style="padding-bottom:30px" t-att-class="this.props.widget.cols +' col-4 block'" t-att-data-id="this.props.widget.id">
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h3><t t-esc="this.props.widget.name"/></h3>
</div>
<div class="col">
<div style="float:right;"><i title="Configuration" class="fa fa-cog block_setting fa-2x cursor-pointer" t-on-click="getConfiguration"/></div>
</div>
</div>
</div>
<div class="card-body" id="in_ex_body_hide">
<div class="row">
<div class="col-md-12 chart_canvas">
<div id="chart_canvas">
<canvas t-ref="chart"/>
</div>
</div>
</div>
</div>
</div>
</div>
`

52
odoo_dynamic_dashboard/static/src/js/DynamicDashboardTile.js

@ -1,52 +0,0 @@
/** @odoo-module */
import { registry} from '@web/core/registry';
const { Component, xml } = owl
export class DynamicDashboardTile extends Component {
setup() {
super.setup(...arguments);
this.action = this.props.doAction
}
async getRecord() {
var model_name = this.props.widget.model_name
if (model_name){
await this.action.doAction({
type: 'ir.actions.act_window',
res_model: model_name,
view_mode: 'tree',
views: [[false, "tree"]],
domain: this.props.widget.domain,
});
}
}
async getConfiguration() {
var id = this.props.widget.id
await this.action.doAction({
type: 'ir.actions.act_window',
res_model: 'dashboard.block',
res_id: id,
view_mode: 'form',
views: [[false, "form"]]
});
}
}
DynamicDashboardTile.template = xml`<div class="col-sm-12 col-md-12 col-lg-3 tile block"
t-att-data-id="this.props.widget.id">
<div draggable="true" t-att-style="'background: ' + this.props.widget.tile_color " class="tile-container d-flex justify-content-around align-items-center position-relative w-100 h-auto my-3">
<a t-on-click="getConfiguration" class="block_setting position-absolute tile-container__setting-icon cursor-pointer">
<i class="fa fa-cog"></i>
</a>
<div t-on-click="getRecord" class="d-flex cursor-pointer">
<div t-att-style="'color: ' + this.props.widget.icon_color " class="tile-container__icon-container bg-white d-flex justify-content-center align-items-center">
<i t-att-class="this.props.widget.icon" aria-hidden="true"></i>
</div>
<div class="tile-container__status-container" t-att-style="'color: ' + this.props.widget.text_color ">
<h2 class="status-container__title" t-att-style="'color: ' + this.props.widget.text_color "><t t-esc="this.props.widget.name"/></h2>
<div class="status-container__figures d-flex flex-wrap align-items-baseline">
<h3 class="mb-0 mb-md-1 mb-lg-0 mr-1" t-att-style="'color: ' + this.props.widget.text_color "><t t-esc="this.props.widget.value"/></h3>
</div>
</div>
</div>
</div>
</div>`

391
odoo_dynamic_dashboard/static/src/js/dynamic_dashboard.js

@ -0,0 +1,391 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { loadJS } from '@web/core/assets';
import { DynamicDashboardTile} from './dynamic_dashboard_tile';
import { DynamicDashboardChart} from './dynamic_dashboard_chart';
import { useService } from "@web/core/utils/hooks";
const { Component, useRef, mount, onWillStart, onMounted} = owl;
export class OdooDynamicDashboard extends Component {
// Setup function to run when the template of the class OdooDynamicDashboard renders
setup() {
this.ThemeSelector = useRef('ThemeSelector');
this.action = useService("action");
this.orm = useService("orm");
this.dialog = useService("dialog");
this.actionId = this.props.actionId
this.rpc = useService("rpc");
onWillStart(async () => {
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js")
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js")
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js")
})
onMounted(()=>{
this.renderDashboard();
})
}
onChangeTheme(){
/* Function for changing color of the theme of the dashboard */
$(".container").attr('style', this.ThemeSelector.el.value + 'min-height:-webkit-fill-available;')
}
ResizeDrag() {
/* Function for resizing and dragging the div resize-drag */
$('.items .resize-drag').each(function(index, element) {
interact(element).resizable({
edges: { left: true, right: true, bottom: true, top: true },
listeners: {
move (event) {
var target = event.target
var x = (parseFloat(target.getAttribute('data-x')) || 0)
var y = (parseFloat(target.getAttribute('data-y')) || 0)
// update the element's style
target.style.width = event.rect.width + 'px'
target.style.height = event.rect.height + 'px'
// translate when resizing from top or left edges
x += event.deltaRect.left
y += event.deltaRect.top
}
},
modifiers: [
// keep the edges inside the parent
interact.modifiers.restrictEdges({
outer: 'parent'
}),
// minimum size
interact.modifiers.restrictSize({
min: { width: 100, height: 50 }
})
],
inertia: true
}).draggable({
listeners: {move: dragMoveListener},
inertia: true,
modifiers: [
interact.modifiers.restrictRect({
restriction: 'parent',
endOnly: true
})
]
})
function dragMoveListener (event) {
var target = event.target
// keep the dragged position in the data-x/data-y attributes
var x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
var y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy
// translate the element
target.style.transform = 'translate(' + x + 'px, ' + y + 'px)'
// update the posiion attributes
target.setAttribute('data-x', x)
target.setAttribute('data-y', y)
}
// this function is used later in the resizing
window.dragMoveListener = dragMoveListener
});
}
async renderDashboard(){
/* Function for rendering the dashboard */
var self = this;
$("#save_layout").hide();
await this.orm.call('dashboard.theme', 'get_records', [[]]).then(function (response) {
response.forEach((ev) => {
const options = document.createElement("option");
options.value = ev.style;
options.text = ev.name;
self.ThemeSelector.el.append(options)
});
})
await this.orm.call("dashboard.block", "get_dashboard_vals", [[], this.actionId]).then( function (response){
for (let i = 0; i < response.length; i++) {
if (response[i].type === 'tile'){
mount(DynamicDashboardTile, $('.items')[0], { props: {
widget: response[i], doAction: self.action, dialog:self.dialog, orm: self.orm
}});
}
else{
mount(DynamicDashboardChart, $('.items')[0], { props: {
widget: response[i], doAction: self.action, rpc: self.rpc, dialog:self.dialog, orm: self.orm
}});
}
}
})
}
editLayout(ev) {
/* Function for editing the layout , it enables resizing and dragging functionality */
$('.items .resize-drag').each(function(index, element) {
interact(element).draggable(true)
interact(element).resizable(true)
});
ev.stopPropagation();
ev.preventDefault();
$("#edit_layout").hide();
$("#save_layout").show();
this.ResizeDrag()
}
saveLayout(ev){
/* Function for saving the layout */
var self = this;
ev.stopPropagation();
ev.preventDefault();
$("#edit_layout").show();
$("#save_layout").hide();
var data_list = []
$('.items .resize-drag').each(function(index, element) {
interact(element).draggable(false)
interact(element).resizable(false)
data_list.push({
'id' : element.dataset['id'],
'data-x': element.dataset['x'],
'data-y': element.dataset['y'],
'height': element.clientHeight,
'width': element.clientWidth,
})
});
self.orm.call('dashboard.block','get_save_layout', [[], data_list]).then( function (response){
window.location.reload();
});
}
changeViewMode(ev){
/* Function for changing the mode of the view */
ev.stopPropagation();
ev.preventDefault();
const currentMode = $(".mode").attr("mode");
if (currentMode == "light"){
$('.theme').attr('style','display: none;')
$(".container").attr('style', 'background-color: #383E45;min-height:-webkit-fill-available; !important')
$(".mode").attr("mode", "dark")
$(".bi-moon-stars-fill").attr('class', 'bi bi-cloud-sun-fill view-mode-icon')
$(".bi-cloud-sun-fill").attr('style', 'color:black;margin-left:10px;')
$(".mode").attr('style','display: none !important');
$("#search-input-chart").attr('style', 'background-color: white; !important')
$("#search-button").attr('style', 'background-color: #BB86FC; !important')
$("#dropdownMenuButton").attr('style', 'background-color: #03DAC5;margin-top:-4px; !important')
$("#text_add").attr('style', 'color: black; !important')
$(".date-label").attr('style', 'color: black;font-family:monospace; !important')
$(".block_setting").attr('style', 'color: white; !important')
$(".block_delete").attr('style', 'color: white; !important')
$(".block_image").attr('style', 'color: #03DAC5; !important')
$(".block_pdf").attr('style', 'color: #03DAC5; !important')
$(".block_csv").attr('style', 'color: #03DAC5; !important')
$(".block_xlsx").attr('style', 'color: #03DAC5; !important')
}
else {
$('.theme').attr('style','display: block;')
$(".container").attr('style', this.ThemeSelector.el.value + 'min-height:-webkit-fill-available;')
$(".mode").attr("mode", "light")
$(".bi-cloud-sun-fill").attr('class', 'bi bi-moon-stars-fill view-mode-icon')
$(".view-mode-icon").attr('style', 'color:black;margin-left:10px; !important')
$(".mode").attr('style','display: none !important');
$(".mode").attr('style','color: white !important');
$("#search-input-chart").attr('style', 'background-color: none; !important')
$("#search-button").attr('style', 'background-color: none; !important')
$("#dropdownMenuButton").attr('style', 'background-color: none;margin-top:-4px; !important')
$("#text_add").attr('style', 'color: white; !important')
$(".date-label").attr('style', 'color: black; !important;font-family:monospace; !important')
$(".block_setting").attr('style', 'color: black; !important')
$(".block_delete").attr('style', 'color: black; !important')
$(".block_image").attr('style', 'color: black; !important')
$(".block_pdf").attr('style', 'color: black; !important')
$(".block_csv").attr('style', 'color: black; !important')
$(".block_xlsx").attr('style', 'color: black; !important')
}
}
onClickAdd(event){
/* For enabling the toggle button */
event.stopPropagation();
event.preventDefault();
$(".dropdown-addblock").toggle()
}
onClickAddItem(event){
/* Function for adding tiles and charts */
event.stopPropagation();
event.preventDefault();
self = this;
var type = event.target.getAttribute('data-type');
if (type == 'graph'){
var chart_type = event.target.getAttribute('data-chart_type');
}
if (type == 'tile'){
var randomColor = '#' + ('000000' + Math.floor(Math.random() * 16777216).toString(16)).slice(-6);
this.action.doAction({
type: 'ir.actions.act_window',
res_model: 'dashboard.block',
view_mode: 'form',
views: [[false, 'form']],
context: {
'form_view_initial_mode': 'edit',
'default_name': 'New Tile',
'default_type': type,
'default_height': '155px',
'default_width': '300px',
'default_tile_color': randomColor,
'default_text_color': '#FFFFFF',
'default_val_color': '#F3F3F3',
'default_fa_icon': 'fa fa-bar-chart',
'default_client_action_id': parseInt(self.actionId)
}
})
}
else{
this.action.doAction({
type: 'ir.actions.act_window',
res_model: 'dashboard.block',
view_mode: 'form',
views: [[false, 'form']],
context: {
'form_view_initial_mode': 'edit',
'default_name': 'New ' + chart_type,
'default_type': type,
'default_height': '565px',
'default_width': '588px',
'default_graph_type': chart_type,
'default_fa_icon': 'fa fa-bar-chart',
'default_client_action_id': parseInt(self.actionId)
},
})
}
}
dateFilter(){
/* Function for filtering the data based on the creation date */
$(".items").empty();
var start_date = $("#start-date").val();
var end_date = $("#end-date").val();
var self = this;
if (!start_date){
start_date = "null"
}
if (!end_date){
end_date = "null"
}
this.orm.call("dashboard.block", "get_dashboard_vals", [[], this.actionId, start_date, end_date]).then( function (response){
for (let i = 0; i < response.length; i++) {
if (response[i].type === 'tile'){
mount(DynamicDashboardTile, $('.items')[0], { props: {
widget: response[i], doAction: self.action, dialog:self.dialog, orm: self.orm
}});
}
else{
mount(DynamicDashboardChart, $('.items')[0], { props: {
widget: response[i], doAction: self.action, rpc: self.rpc, dialog:self.dialog, orm: self.orm
}});
}
}
})
}
async clickSearch(){
/* Function for searching the blocks with their names */
var input = $("#search-input-chart").val();
await this.rpc('/custom_dashboard/search_input_chart', {'search_input': input}).then(function (response) {
var blocks = $(".items .resize-drag");
blocks.each(function(index, element){
var dataId = $(element).data('id');
if (response.includes(dataId)){
$(element).css("visibility", "visible");
}
else{
$(element).css("visibility", "hidden");
}
})
})
}
showViewMode(ev){
/* Function for showing the mode text */
const currentMode = $(".mode").attr("mode");
if (currentMode == "light"){
$(".mode").text("Dark Mode")
$(".mode").attr('style','display: inline-block !important; color: black !important');
}
else{
$(".mode").text("Light Mode")
$(".mode").attr('style','display: inline-block !important; color: black !important');
}
}
hideViewMode(ev){
/* Function for hiding the mode text */
$(".mode").fadeOut(2000);
}
clearSearch(){
/* Function for clearing the search input */
$("#search-input-chart").val('');
var blocks = $(".items .resize-drag");
blocks.each(function(index, element){
$(element).css("visibility", "visible");
})
}
async printPdf() {
/* Function for printing whole dashboard in pdf format */
var elements = $('.items .resize-drag')
var newElement = document.createElement('div');
newElement.className = 'pdf';
elements.each(function(index, elem){
newElement.appendChild(elem);
});
for (var x=0; x< $(newElement)[0].children.length; x++){
$($(newElement)[0].children[x])[0].style.transform = ""
}
var opt = {
margin: 0.3,
filename: 'Dashboard.pdf',
image: { type: 'jpeg', quality: 1 },
html2canvas: { scale: 1 },
jsPDF: { unit: 'mm', format: 'a3', orientation: 'portrait' }
};
html2pdf().set(opt).from(newElement).save().then(()=>{
window.location.reload()
})
}
async createPDF(){
/* Function for getting pdf data in string format */
var elements = $('.items .resize-drag')
var newElement = document.createElement('div');
newElement.className = 'pdf';
elements.each(function(index, elem){
newElement.appendChild(elem);
});
for (var x=0; x< $(newElement)[0].children.length; x++){
$($(newElement)[0].children[x])[0].style.transform = ""
}
var opt = {
margin: 0.3,
filename: 'Dashboard.pdf',
image: { type: 'jpeg', quality: 1 },
html2canvas: { scale: 1 },
jsPDF: { unit: 'mm', format: 'a3', orientation: 'portrait' }
};
var pdf = html2pdf().set(opt).from(newElement).toPdf()
var pdfOutput = await pdf.output('datauristring');
console.log(pdfOutput)
return pdfOutput
}
async sendMail(){
/* Function for creating pdf and sending mail to the selected users */
/* This function calls the createPDF() function and returns the pdf datas */
var created_pdf = await this.createPDF();
var base64code = created_pdf.split(',')[1];
this.action.doAction({
type: 'ir.actions.act_window',
name: 'SEND MAIL',
res_model: 'dashboard.mail',
view_mode: 'form',
views: [[false, 'form']],
target: 'new',
context: {
'default_base64code': base64code,
}
})
}
}
OdooDynamicDashboard.template = "owl.OdooDynamicDashboard"
registry.category("actions").add("OdooDynamicDashboard", OdooDynamicDashboard)

215
odoo_dynamic_dashboard/static/src/js/dynamic_dashboard_chart.js

@ -0,0 +1,215 @@
/** @odoo-module **/
import { loadJS } from '@web/core/assets';
import { getColor } from "@web/core/colors/colors";
import { _t } from "@web/core/l10n/translation";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
const { Component, xml, onWillStart, useRef, onMounted } = owl
export class DynamicDashboardChart extends Component {
// Setup function of the class DynamicDashboardChart
setup() {
this.doAction = this.props.doAction.doAction;
this.chartRef = useRef("chart");
this.dialog = this.props.dialog;
onWillStart(async () => {
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js")
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js")
await loadJS("https://cdn.jsdelivr.net/npm/exceljs@4.4.0/dist/exceljs.min.js")
})
onMounted(()=> this.renderChart())
}
// Function to export the chart in pdf, image, xlsx and csv format
exportItem(ev){
ev.stopPropagation();
ev.preventDefault();
var type = $(ev.currentTarget).attr('data-type');
var canvas = $($($(ev.currentTarget)[0].offsetParent)[0].children[0].lastChild).find("#canvas")[0]
var dataTitle = $(canvas).attr('data-title')
var bgCanvas = document.createElement("canvas");
bgCanvas.width = canvas.width;
bgCanvas.height = canvas.height;
var bgCtx = bgCanvas.getContext("2d");
bgCtx.fillStyle = "white";
bgCtx.fillRect(0, 0, canvas.width, canvas.height);
bgCtx.drawImage(canvas, 0, 0);
var imgData = bgCanvas.toDataURL("image/png");
if (type === 'png') {
var downloadLink = document.createElement('a');
downloadLink.href = imgData;
downloadLink.download = `${dataTitle}.png`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
if (type === 'pdf') {
var pdf = new jsPDF();
pdf.addImage(imgData, 'PNG', 0, 0);
pdf.save(`${dataTitle}.pdf`);
}
if (type === 'xlsx'){
var rows = [];
var items = $('.resize-drag');
for (let i = 0; i < items.length; i++) {
if ($(items[i]).attr('data-id') === $(ev.currentTarget).attr('data-id')) {
rows.push(this.props.widget.x_axis);
rows.push(this.props.widget.y_axis);
}
}
// Prepare the workbook
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('My Sheet');
for(let i = 0; i < rows.length; i++){
worksheet.addRow(rows[i]);
}
const image = workbook.addImage({
base64: imgData,
extension: 'png',
});
worksheet.addImage(image, {
tl: { col: 0, row: 4 },
ext: { width: canvas.width, height: canvas.height }
});
// Save workbook to a file
workbook.xlsx.writeBuffer()
.then((buffer) => {
// Create a Blob object from the buffer
let blob = new Blob([buffer], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
let link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.setAttribute("download", `${dataTitle}.xlsx`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
}
if (type === 'csv') {
var rows = [];
var items = $('.resize-drag')
for (let i = 0; i < items.length; i++) {
if ($(items[i]).attr('data-id') === $(ev.currentTarget).attr('data-id')) {
rows.push(this.props.widget.x_axis);
rows.push(this.props.widget.y_axis);
}
}
let csvContent = "data:text/csv;charset=utf-8,";
rows.forEach(function (rowArray) {
let row = rowArray.join(",");
csvContent += row + "\r\n";
});
var link = document.createElement("a");
link.setAttribute("href", encodeURI(csvContent));
link.setAttribute("download", `${dataTitle}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
// Function to get the configuration of the chart
async getConfiguration(ev){
ev.stopPropagation();
ev.preventDefault();
var id = this.props.widget.id
await this.doAction({
type: 'ir.actions.act_window',
res_model: 'dashboard.block',
res_id: id,
view_mode: 'form',
views: [[false, "form"]]
});
}
// Function to remove the chart
async removeTile(ev){
ev.stopPropagation();
ev.preventDefault();
this.dialog.add(ConfirmationDialog, {
title: _t("Delete Confirmation"),
body: _t("Are you sure you want to delete this item?"),
confirmLabel: _t("YES, I'M SURE"),
cancelLabel: _t("NO, GO BACK"),
confirm: async () => {
await this.props.orm.unlink("dashboard.block", [this.props.widget.id]);
location.reload();
},
cancel: () => {},
});
}
// Function to render the chart
renderChart(){
if (this.props.widget.graph_type){
const x_axis = this.props.widget.x_axis
const y_axis = this.props.widget.y_axis
const data = []
for (let i = 0; i < x_axis.length; i++) {
const value = { key: x_axis[i], value: y_axis[i] }
data.push(value);
}
new Chart(
this.chartRef.el,
{
type: this.props.widget.graph_type || 'bar',
data: {
labels: data.map(row => row.key),
datasets: [
{
label: this.props.widget.measured_field || 'Data',
data: data.map(row => row.value),
backgroundColor: data.map((_, index) => getColor(index)),
hoverOffset : 4
}
]
},
}
);
}
}
}
DynamicDashboardChart.template = xml`
<div class="resize-drag block card"
t-att-data-x="this.props.widget.data_x"
t-att-data-y="this.props.widget.data_y"
t-att-style="'height:'+this.props.widget.height+'; width:'+ this.props.widget.width+ '; transform: translate('+ this.props.widget.translate_x +', '+ this.props.widget.translate_y +');'"
t-att-data-id="this.props.widget.id">
<div class="card-body mt-1" id="in_ex_body_hide">
<div class="block_edit block_setting" t-on-click="(ev) => this.getConfiguration(ev)">
<i title="Configuration"
class="fa fa-pencil block_setting chart-edit"/>
</div>
<div class="block_edit block_image" data-type="png" t-on-click="(ev) => this.exportItem(ev)">
<i title="Save As Image"
class="bi bi-image block_image chart-image"/>
</div>
<div class="block_edit block_pdf" data-type="pdf" t-on-click="(ev) => this.exportItem(ev)">
<i title="Export to PDF"
class="bi bi-file-earmark-pdf block_pdf chart-pdf"/>
</div>
<div class="block_edit block_csv" t-att-data-id="this.props.widget.id" data-type="csv" t-on-click="(ev) => this.exportItem(ev)">
<i title="Export to CSV"
class="bi bi-filetype-csv block_csv chart-csv"/>
</div>
<div class="block_edit block_xlsx" t-att-data-id="this.props.widget.id" data-type="xlsx" t-on-click="(ev) => this.exportItem(ev)">
<i title="Export to XLSX"
class="fa fa-file-excel-o block_xlsx chart-xlsx"/>
</div>
<div class="block_edit block_delete" t-on-click="(ev) => this.removeTile(ev)">
<i title="Delete"
class="fa fa-times block_delete chart-setting"/>
</div>
<h3 class="chart_title">
<t t-esc="this.props.widget.name"/>
</h3>
<div class="row">
<div class="col-md-12 chart_canvas" id="chart_canvas"
t-att-data-id="this.props.widget.id">
<canvas id="canvas" t-ref="chart" t-att-data-title="this.props.widget.name"/>
</div>
</div>
</div>
</div>
`

89
odoo_dynamic_dashboard/static/src/js/dynamic_dashboard_tile.js

@ -0,0 +1,89 @@
/** @odoo-module **/
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
const { Component, xml } = owl;
export class DynamicDashboardTile extends Component {
// Setup function of the class DynamicDashboardTile
setup() {
this.doAction = this.props.doAction.doAction;
this.dialog = this.props.dialog;
this.orm = this.props.orm;
}
// Function to get the configuration of the tile
async getConfiguration(ev){
ev.stopPropagation();
ev.preventDefault();
var id = this.props.widget.id
await this.doAction({
type: 'ir.actions.act_window',
res_model: 'dashboard.block',
res_id: id,
view_mode: 'form',
views: [[false, "form"]]
});
}
// Function to remove the tile
async removeTile(ev){
ev.stopPropagation();
ev.preventDefault();
this.dialog.add(ConfirmationDialog, {
title: _t("Delete Confirmation"),
body: _t("Are you sure you want to delete this item?"),
confirmLabel: _t("YES, I'M SURE"),
cancelLabel: _t("NO, GO BACK"),
confirm: async () => {
await this.orm.unlink("dashboard.block", [this.props.widget.id]);
location.reload();
},
cancel: () => {},
});
}
// Function for getting records by double click
async getRecords(){
var model_name = this.props.widget.model_name;
if (model_name){
await this.doAction({
type: 'ir.actions.act_window',
res_model: model_name,
view_mode: 'tree',
views: [[false, "tree"]],
domain: this.props.widget.domain,
});
}
}
}
DynamicDashboardTile.template = xml `
<div class="resize-drag tile"
t-on-dblclick="getRecords"
t-att-data-id="this.props.widget.id"
t-att-data-x="this.props.widget.data_x"
t-att-data-y="this.props.widget.data_y"
t-att-style="this.props.widget.color+this.props.widget.text_color+ 'height:'+this.props.widget.height+';width:'+this.props.widget.width + '; transform: translate('+ this.props.widget.translate_x +', '+ this.props.widget.translate_y +');'">
<div t-att-style="this.props.widget.color+this.props.widget.text_color"
class="d-flex align-items-center w-100 my-3">
<a class="block_setting tile_edit tile-container__setting-icon" style="color:black;" t-on-click="(ev) => this.getConfiguration(ev)" >
<i class="fa fa-edit"/>
</a>
<a class="block_delete tile_edit tile-container__delete-icon" style="color:black;" t-on-click="(ev) => this.removeTile(ev)">
<i class="fa fa-times"/>
</a>
<div t-att-style="this.props.widget.icon_color"
class="tile-container__icon-container bg-white d-flex justify-content-center align-items-center">
<i t-att-class="this.props.widget.icon"/>
</div>
<div t-att-style="this.props.widget.text_color"
class="tile-container__status-container">
<h2 t-att-style="this.props.widget.text_color"
class="status-container__title">
<t t-esc="this.props.widget.name"/>
</h2>
<div class="status-container__figures d-flex flex-wrap align-items-baseline">
<h3 class="mb-0 mb-md-1 mb-lg-0 mr-1"
t-att-style="this.props.widget.val_color">
<t t-esc="this.props.widget.value"/>
</h3>
</div>
</div>
</div>
</div>`

208
odoo_dynamic_dashboard/static/src/scss/dynamic_dashboard.scss

@ -0,0 +1,208 @@
:root {
/* Colors */
--green: #00C689;
--blue: #3DA5F4;
--red: #F1536E;
--yellow: #FDA006;
/*Fonts*/
--primary-font: 'Roboto', sans-serif;
}
html .o_web_client > .o_action_manager {
overflow: auto;
}
.bg-green {
background-color: var(--green);
}
.bg-blue {
background-color: var(--blue);
}
.bg-red {
background-color: var(--red);
}
.bg-yellow {
background-color: var(--yellow);
}
.text-color-yellow {
color: var(--yellow);
}
.text-color-green {
color: var(--green);
}
.text-color-blue {
color: var(--blue);
}
.text-color-red {
color: var(--red);
}
.text-color-yellow {
color: var(--yellow);
}
.tile-container__icon-container {
border-radius: 50%;
width: 4.75rem;
height: 4.75rem;
font-size: 28px;
margin-left: 15px;
}
.tile-container__status-container {
margin-left: 2em;
}
.status-container__title {
font-family: var(--primary-font);
font-weight: 500;
font-size: 1.5rem;
line-height: 1.5rem;
}
.status-container__figures {
font-family: var(--primary-font);
}
.status-container__figures > h3 {
font-weight: 700;
font-size: 1.5rem;
line-height: 1.813rem;
}
.tile-container__setting-icon {
top: 0.638rem;
}
.dark-theme {
/* Add dark theme styles here */
.block_edit {
color: #b7b7b7 !important;
}
.theme_icon:hover {
text-shadow: 0px 0px 5px #9388ff;
}
.add_block{
color: #dfdfdf;
}
#ExportMenu {
color: #626262;
}
.chart_title {
color: #dfdfdf;
margin: -0.5em 0 2em 0;
}
.btn-search_edit, {
color: #313131;
border: none;
background-color: #979797 !important;
}
#search-input-chart {
color: #ffffff;
border: 1px solid #4e4e4e;
background-color: #2A2A2A !important;
}
#search-clear {
color: #1f1f1f !important;
}
div.navbar {
color: #909090;
background-color: #2A2A2A !important;
}
.navbar-collapse {
margin-bottom: 12px !important;
}
h3 {
color: #909090;
}
div.dropdown-addblock {
color: #909090;
background-color: #2A2A2A !important;
}
#dropdownMenuButton {
color: #313131;
background-color: #979797 !important;
}
div.card-body {
border-radius: 0.5em !important;
background-color: #2a2a2a !important;
}
.o_kanban_record {
background-color: #e8e8e8 !important;
}
.o_kanban_renderer {
background-color: #e8e8e8 !important;
}
.oe_module_name {
background-color: #ffffff !important;
}
background-color: #1C1B1B;
@media (max-width: 767px) {
.navbar-light .navbar-toggler {
color: #A7A7A72D;
border-color: #F6F6F621;
}
}
}
.btn-align-items{
width: 134px;
font-size: small;
border-radius: revert;
height: 33px;
}
#edit_layout{
background-color: #0c8444;
}
#save_layout{
background-color: #b53c5d;
width: 134px;
height: 33px;
font-size: small;
border-radius: revert;
}
#search-button{
width: 69px;
margin-left: 5px;
}
#search-input-chart{
width: 206px;
height: 34px;
border: 1px solid black;
}
.search-clear{
margin-left: -80px;
}
label input{
appearance: none;
}
.mode{
padding-left: 7px;
font-family: 'odoo_ui_icons';
display: none;
}
.view-mode-icon{
font-size: x-large;
}

109
odoo_dynamic_dashboard/static/src/scss/style.scss

@ -1,109 +0,0 @@
.card{
border: none;
border-radius: 0px;
box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
-webkit-box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
-moz-box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
}
.card-header{
background-color: transparent;
border: none;
}
:root {
/* Colors */
--green: #00C689;
--blue: #3DA5F4;
--red: #F1536E;
--yellow: #FDA006;
/*Fonts*/
--primary-font: 'Roboto', sans-serif;
}
html .o_web_client > .o_action_manager {
overflow:auto;
}
.bg-green {
background-color: var(--green);
}
.bg-blue {
background-color: var(--blue);
}
.bg-red {
background-color: var(--red);
}
.bg-yellow {
background-color: var(--yellow);
}
.text-color-yellow {
color: var(--yellow);
}
.text-color-green {
color: var(--green);
}
.text-color-blue {
color: var(--blue);
}
.text-color-red {
color: var(--red);
}
.text-color-yellow {
color: var(--yellow);
}
.tile-container {
padding: 3.2rem 1.5rem;
border-radius: 2rem;
}
.tile-container__icon-container {
border-radius: 50%;
width: 4.75rem;
height: 4.75rem;
font-size: 28px;
}
.status-container__title {
font-family: var(--primary-font);
font-weight: 500;
font-size: 1.5rem;
line-height: 1.5rem;
}
.status-container__figures {
font-family: var(--primary-font);
}
.status-container__figures>h3 {
font-weight: 700;
font-size: 1.5rem;
line-height: 1.813rem;
}
.tile-container__setting-icon {
top: 0.638rem;
right: 1rem;
}
// Main Navbar Dropdown menu location override
.o_menu_systray > .o_user_menu > .o-dropdown--menu{
left: auto !important;
right: 0px !important;
}
.button-container{
padding-top: 5px;
}
.add_block{
margin-left: 6px;
}
.tile-container__status-container{
padding-left: 7px;
}

132
odoo_dynamic_dashboard/static/src/xml/dynamic_dashboard_template.xml

@ -1,21 +1,129 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="owl.dynamic_dashboard" owl="1">
<div class="container">
<div class="button-container">
<button class="btn btn-primary" data-type="tile"
type="button" t-on-click="_onClick_add_block">Add Block
<!--DASHBOARD VIEW WITH NAVIGATION-BAR, INTERACTJS TEMPLATE-->
<t t-name="owl.OdooDynamicDashboard" owl="1">
<div class="container" style="min-height:-webkit-fill-available;">
<div class="navbar navbar-expand-md navbar-light mb-4 navbar-style border-bottom"
role="navigation">
<button class="navbar-toggler" id="dropdownNavbar" type="button"
data-toggle="collapse"
data-target="#navbarCollapse"
aria-controls="navbarCollapse" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"/>
</button>
<button class="btn btn-primary add_block" data-type="graph"
type="button" t-on-click="_onClick_add_block">Add Graph
</button>
</div>
<div class="o_dynamic_dashboard row">
<div class="o_dynamic_tile row">
<div class="collapse navbar-collapse"
aria-labelledby="dropdownNavbar">
<ul class="navbar-nav mr-auto">
<label class="navbar-items dropdown drop-down-add">
<button class="btn btn-align-items dropdown-add-items dropdown-toggle"
style="margin-top:-4px;"
type="selection" id="dropdownMenuButton"
data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false"
t-on-click="onClickAdd">
<i class="fa fa-plus-circle"/>
<span id="text_add">⠀Add Items</span>
</button>
<div class="dropdown-menu dropdown-addblock"
aria-labelledby="dropdownMenuButton">
<a class="dropdown-item add_block"
data-type="tile"
t-on-click="(ev) => this.onClickAddItem(ev)">Tile</a>
<a class="dropdown-item add_block"
data-type="graph" data-chart_type="bar"
t-on-click="(ev) => this.onClickAddItem(ev)">Bar Chart</a>
<a class="dropdown-item add_block"
data-type="graph" data-chart_type="doughnut"
t-on-click="(ev) => this.onClickAddItem(ev)">Doughnut Chart</a>
<a class="dropdown-item add_block"
data-type="graph"
data-chart_type="line"
t-on-click="(ev) => this.onClickAddItem(ev)">Line Chart</a>
<a class="dropdown-item add_block"
data-type="graph" data-chart_type="pie"
t-on-click="(ev) => this.onClickAddItem(ev)">Pie Chart</a>
<a class="dropdown-item add_block"
data-type="graph"
data-chart_type="polarArea"
t-on-click="(ev) => this.onClickAddItem(ev)">Polar Area Chart</a>
<a class="dropdown-item add_block"
data-type="graph"
data-chart_type="radar"
t-on-click="(ev) => this.onClickAddItem(ev)">Radar Chart</a>
</div>
</label>
</ul>
</div>
<label class="navbar-items layout-switch"
style="padding-top:20px;"
id="edit-layout-label">
<button class="navbar-items btn-search_edit btn-align-items btn btn-primary my-2 mx-2 my-sm-0"
type="button"
id="edit_layout"
t-on-click="(ev) => this.editLayout(ev)">Edit Layout</button>
<button class="navbar-items btn-search_edit btn btn-primary my-2 mx-2 my-sm-0"
type="button"
id="save_layout"
t-on-click="(ev) => this.saveLayout(ev)">Save Layout</button>
<label for="view_mode"
t-on-mouseover="(ev) => this.showViewMode(ev)"
t-on-mouseout="(ev) => this.hideViewMode(ev)"
t-on-click="(ev) => this.changeViewMode(ev)"
style="margin-left: 6px;">
<input type="checkbox" id="view_mode"/>
<span><i class="bi bi-moon-stars-fill view-mode-icon" style="margin-left:10px"/></span>
</label>
<span class="mode" mode="light">Dark mode</span>
</label>
<div class="search-group" style="margin-right: 30px;padding-top:20px;">
<!-- Search Bar -->
<div class="navbar-items btn-group search-box">
<input class="form-control mr-sm-2" type="text" placeholder="Search" id="search-input-chart" aria-label="Search"/>
<span id="searchclear" t-on-click="clearSearch">
<i class="fa fa-times search-clear"
style="margin-left:-25px;margin-top:9px;"/>
</span>
</div>
<button class="btn btn-outline-success my-2 my-sm-0"
t-on-click="clickSearch"
style="margin-left:10px;"
type="button">Search
</button>
</div>
<!-- Date Inputs -->
<div class="date-inputs"
style="position: absolute; right: 34.5em; font-size: smaller;font-family: monospace; padding-top:inherit; ">
<label for="start-date" class="date-label"
style="color: black;">Start Date:</label>
<input type="date" id="start-date" name="start-date"
t-on-change="dateFilter"
style="color: black; border: 1px solid #4e4e4e; background-color: white; padding: 5px 10px; border-radius: 5px;"/>
<label for="end-date" class="date-label"
style="color: black; margin-left: 10px;">End Date:</label>
<input type="date" id="end-date" name="end-date"
t-on-change="dateFilter"
style="color: black; border: 1px solid #4e4e4e; background-color: white; padding: 5px 10px; border-radius: 5px;"/>
</div>
<div class="o_dynamic_graph w3-container row">
<div class="o-dropdown dropdown theme">
<select class="form-select"
t-ref="ThemeSelector"
t-on-change="onChangeTheme">
<option value="0">Select Theme</option>
</select>
</div>
<div class="dashboard_pdf" t-on-click="printPdf">
<i class="bi bi-filetype-pdf" style="font-size:24px;"/>
</div>
<div class="dashboard_mail" t-on-click="sendMail">
<i class="bi bi-envelope-fill" style="font-size:24px;"/>
</div>
</div>
<div class="all_items" style="display:grid;">
<div class="items"/>
</div>
<!--CONTAINER FOR CONTENT GENERATION :TILE & CHART(FROM DynamicDashboardTile & DynamicDashboardChart-->
</div>
</t>
</templates>

46
odoo_dynamic_dashboard/views/dashboard_menu_view.xml

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form view of the model dashboard menu-->
<record id="dashboard_menu_view_form" model="ir.ui.view">
<field name="name">dashboard.menu.view.form</field>
<field name="model">dashboard.menu</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name"/>
<field name="parent_id"/>
<field name="group_ids" widget="many2many_tags" invisible="1"/>
<field name="client_action_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Tree view of the model dashboard menu -->
<record id="dashboard_menu_view_tree" model="ir.ui.view">
<field name="name">dashboard.menu.view.tree</field>
<field name="model">dashboard.menu</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="parent_id"/>
</tree>
</field>
</record>
<!-- Action for the model dashboard menu -->
<record id="dashboard_menu_action" model="ir.actions.act_window">
<field name="name">Dashboard Menu</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">dashboard.menu</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Menu item to show the configuration in module dynamic dashboard-->
<menuitem name="Configuration" id="menu_dynamic_dashboard_configuration" parent="odoo_dynamic_dashboard.menu_dashboard"
sequence="3"/>
<!-- Menu item to show the dynamic menus in module dynamic dashboard-->
<menuitem name="Dashboards" id="menu_dynamic_dashboard_menu" parent="odoo_dynamic_dashboard.menu_dynamic_dashboard_configuration"
sequence="3" action="dashboard_menu_action"/>
</odoo>

66
odoo_dynamic_dashboard/views/dashboard_menu_views.xml

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--Kanban view of the dashboard menu-->
<record id="dashboard_menu_view_kanban" model="ir.ui.view">
<field name="name">dashboard.menu.view.kanban</field>
<field name="model">dashboard.menu</field>
<field name="arch" type="xml">
<kanban>
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<center>
<h3 class="my-2 ms-3">
Name:
<field name="name"/>
</h3>
</center>
<div class="row">
<hr class="mt4 mb4"/>
<div class="col-6 text-center">
<strong>Parent:</strong>
</div>
<div class="col-6 text-center">
<field name="menu_id"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!--Form view of the dashboard menu-->
<record id="dashboard_menu_view_form" model="ir.ui.view">
<field name="name">dashboard.menu.view.form</field>
<field name="model">dashboard.menu</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name" class="oe_inline"/>
<field name="menu_id" class="oe_inline"/>
<field name="group_ids" widget="many2many_tags"
invisible="1"/>
<field name="client_action_id" invisible="1"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action specified for the dashboard menu-->
<record id="dashboard_menu_action" model="ir.actions.act_window">
<field name="name">Dashboards Menu</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">dashboard.menu</field>
<field name="view_mode">kanban,form</field>
</record>
<!--Menu Item of the model Dashboard Menu-->
<menuitem name="Dashboard Menu" id="dashboard_menu_view_action"
parent="odoo_dynamic_dashboard.menu_dashboard"
sequence="10" action="dashboard_menu_action"/>
</odoo>

56
odoo_dynamic_dashboard/views/dashboard_theme_views.xml

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!--Form view of the dashboard block theme-->
<record id="dashboard_theme_view_form" model="ir.ui.view">
<field name="name">dashboard.theme.view.form</field>
<field name="model">dashboard.theme</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<div>
<field name="name" class="oe_inline"
style="font-size: 30px;"
placeholder="Theme Name" required="1"/>
</div>
</group>
<group>
<field name="color_x" widget="color"/>
<field name="color_y" widget="color"/>
<field name="color_z" widget="color"/>
</group>
<notebook>
<page string="Color Gradient">
<field name="body" type="html" readonly="1"/>
<field name="style" invisible="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!--Tree view of the dashboard block theme-->
<record id="dashboard_theme_view_tree" model="ir.ui.view">
<field name="name">dashboard.theme.view.tree</field>
<field name="model">dashboard.theme</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
</tree>
</field>
</record>
<!-- Action specified for the dashboard block theme -->
<record id="dashboard_theme_action" model="ir.actions.act_window">
<field name="name">Dashboard Theme</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">dashboard.theme</field>
<field name="view_mode">tree,form</field>
</record>
<!--Menu Item for the model Dashboard Blocks-->
<menuitem name="Configuration" id="odoo_dynamic_configuration"
parent="odoo_dynamic_dashboard.menu_dashboard"/>
<menuitem name="Dashboard Theme" id="dashboard_theme_menu"
parent="odoo_dynamic_dashboard.odoo_dynamic_configuration"
sequence="45" action="dashboard_theme_action"/>
</odoo>

13
odoo_dynamic_dashboard/views/dashboard_view.xml

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Action to show the dashboard-->
<record id="dynamic_dashboard_action" model="ir.actions.client">
<field name="name">Dashboard</field>
<field name="tag">owl.dynamic_dashboard</field>
</record>
<!-- Menu Item to show the dashboard module-->
<menuitem name="Dashboard" id="menu_dashboard" sequence="0" web_icon="odoo_dynamic_dashboard,static/description/icon.png"/>
<!-- Menu Item to show the dashboards menu in the dashboard module-->
<menuitem name="Dashboards" id="menu_dynamic_dashboard" parent="odoo_dynamic_dashboard.menu_dashboard"
sequence="1" action="dynamic_dashboard_action"/>
</odoo>

14
odoo_dynamic_dashboard/views/dashboard_views.xml

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- The client action record to view the Dashboard-->
<record id="dashboard_view_action" model="ir.actions.client">
<field name="name">Dashboard</field>
<field name="tag">OdooDynamicDashboard</field>
</record>
<menuitem name="Dashboards" id="menu_dashboard" sequence="-1"
web_icon="odoo_dynamic_dashboard,static/description/icon.png"/>
<menuitem name="Dashboard" action="dashboard_view_action"
id="dashboard_root_menu"
parent="odoo_dynamic_dashboard.menu_dashboard"
sequence="0"/>
</odoo>

58
odoo_dynamic_dashboard/views/dynamic_block_view.xml → odoo_dynamic_dashboard/views/dynamic_block_views.xml

@ -1,50 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- form view of the model dashboard block-->
<!--Form view of the dashboard block-->
<record id="dashboard_block_view_form" model="ir.ui.view">
<field name="name">dashboard.block.view.form</field>
<field name="model">dashboard.block</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<div>
<field name="name" class="oe_inline"
style="font-size: 30px;"
placeholder="Block Name" required="1"/>
</div>
</group>
</group>
<group>
<group>
<field name="model_id"
required="[('edit_mode','=', True)]"/>
<field name="client_action" invisible="1"/>
required="[('edit_mode','=', True)]"
options="{'no_create_edit':True,'no_create': True}"/>
<field name="client_action_id" invisible="1"/>
<field name="model_name" invisible="1"/>
<field name="edit_mode" invisible="1"/>
<field name="operation"
required="[('edit_mode','=', True)]"/>
<field name="measured_field"
domain="[('model_id','=',model_id), ('ttype','in',['float','integer','monetary']), ('store', '=', True)]"
required="[('edit_mode','=', True)]"/>
<field name="measured_field_id"
required="[('edit_mode','=', True)]"
options="{'no_create_edit':True, 'no_create': True }"
domain="[('model_id','=',model_id), ('ttype','in',['float','integer','monetary']), ('store', '=', True)]"/>
<field name="filter" widget="domain"
options="{'model': 'model_name'}"/>
</group>
</group>
<group string="Block Information">
<group>
<field name="sequence" invisible="1"/>
<field name="type" required="1"/>
<field name="graph_type"
invisible="type == 'tile'"/>
<field name="graph_size"
invisible="type == 'tile'"/>
<field name="fa_icon" invisible="type == 'graph'"/>
<field name="fa_color" invisible="type == 'graph'"
widget="color"/>
<field name="group_by" invisible="type == 'tile'"
<field name="fa_icon"
invisible="type == 'graph'"/>
<field name="group_by_id" invisible="type == 'tile'"
options="{'no_create_edit':True, 'no_create': True}"
required="[('edit_mode','=', True),('type','=','graph')]"
domain="[('model_id','=',model_id), ('ttype','!=','one2many'), ('store', '=', True)]"/>
<field name="tile_color" invisible="type == 'graph'"
<field name="tile_color"
invisible="type == 'graph'"
widget="color"/>
<field name="val_color"
invisible="type == 'graph'"
widget="color"/>
<field name="text_color" invisible="type == 'graph'"
<field name="text_color"
invisible="type == 'graph'"
widget="color"/>
</group>
</group>
@ -52,7 +60,7 @@
</form>
</field>
</record>
<!-- tree view of the model dashboard block-->
<!--Tree view of the dashboard block-->
<record id="dashboard_block_view_tree" model="ir.ui.view">
<field name="name">dashboard.block.view.tree</field>
<field name="model">dashboard.block</field>
@ -64,7 +72,7 @@
</tree>
</field>
</record>
<!-- Action of the model dashboard block-->
<!-- Action specified for the dashboard block-->
<record id="dashboard_block_action" model="ir.actions.act_window">
<field name="name">Dashboard Block</field>
<field name="type">ir.actions.act_window</field>
@ -72,8 +80,8 @@
<field name="view_mode">tree,form</field>
<field name="context">{'default_edit_mode' : True}</field>
</record>
<!-- Menu item of the model dashboard block-->
<menuitem name="Blocks" id="menu_dynamic_dashboard_blocks"
<!--Menu Item for the model Dashboard Blocks-->
<menuitem name="Dashboard Blocks" id="dashboard_block_menu"
parent="odoo_dynamic_dashboard.menu_dashboard"
sequence="1" action="dashboard_block_action"/>
sequence="5" action="dashboard_block_action"/>
</odoo>

13
odoo_dynamic_dashboard/models/dashboard_block_line.py → odoo_dynamic_dashboard/wizard/__init__.py

@ -19,15 +19,4 @@
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class DashboardBlockLine(models.Model):
""" Creates the model Dashboard Block Line"""
_name = "dashboard.block.line"
_description = "Dashboard Block Line"
sequence = fields.Integer(string="Sequence",
help="Sequence of the block lines")
block_size = fields.Integer(string="Block size",
help="Block size of the dashboard block line")
from . import dashboard_mail

75
odoo_dynamic_dashboard/wizard/dashboard_mail.py

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-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 AFFERO
# GENERAL PUBLIC LICENSE (AGPL 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 AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details.
#
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE
# (AGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class DashboardMail(models.TransientModel):
_name = 'dashboard.mail'
_description = 'Dashboard Mail'
user_ids = fields.Many2many('res.users', string="Users",
domain="[('id','!=', uid)]",
help="Select User")
base64code = fields.Char(string='Base 64', help='Base64 Code of the pdf')
def send_mail(self):
"""
Function for sending mail to the selected users
"""
for user in self.user_ids:
mail_content = (
'Hi %s, <br/> '
'I hope this mail finds you well. I am pleased to share the <b>Dashboard Report</b> with you.<br/>'
'Please find the attachment<br/>') % user.name
mail_values = {
'subject': 'Dashboard Report',
'author_id': self.env.user.partner_id.id,
'body_html': mail_content,
'email_to': user.email,
}
mail_id = self.env['mail.mail'].create(mail_values)
attachment_values = {
'name': 'Dashboard.pdf',
'datas': self.base64code,
'type': 'binary',
'res_model': 'mail.mail',
'res_id': mail_id.id,
}
attachment_id = self.env['ir.attachment'].create(attachment_values)
mail_id.write({
'attachment_ids': [(4, attachment_id.id)]
})
mail_id.send()
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
def cancel_mail(self):
"""
Function for refreshing the page while clicking cancel
"""
return {
'type': 'ir.actions.client',
'tag': 'reload',
}

22
odoo_dynamic_dashboard/wizard/dashboard_mail_views.xml

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Sending dashboard pdf to users -->
<record id="dashboard_mail_view_form" model="ir.ui.view">
<field name="name">dashboard.mail.view.form</field>
<field name="model">dashboard.mail</field>
<field name="arch" type="xml">
<form string="Sent Mail">
<group>
<field name="user_ids" widget="many2many_tags"/>
<field name="base64code" invisible="1"/>
</group>
<footer>
<button name="send_mail" string="SEND"
class="btn-primary" type="object"/>
<button string="Cancel" class="btn-secondary"
name="cancel_mail" type="object"/>
</footer>
</form>
</field>
</record>
</odoo>
Loading…
Cancel
Save