@ -0,0 +1,49 @@ |
|||
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg |
|||
:target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html |
|||
:alt: License: LGPL-3 |
|||
|
|||
Product Management |
|||
================== |
|||
Dashboard which displays top sales, purchases monthly movements and location categorization of Products. |
|||
|
|||
Configuration |
|||
============= |
|||
* No Additional configuration is needed. |
|||
|
|||
Company |
|||
------- |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
|||
|
|||
License |
|||
------- |
|||
Lesser General Public License, v3.0 (LGPL v3). |
|||
(https://www.gnu.org/licenses/lgpl-3.0-standalone.html) |
|||
|
|||
Credits |
|||
------- |
|||
* Developers : (V16) Neethu UM, |
|||
(V17) Kailas Krishna, |
|||
(V18) Aysha Shalin |
|||
Contact: odoo@cybrosys.com |
|||
|
|||
Contacts |
|||
-------- |
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Bug Tracker |
|||
----------- |
|||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. |
|||
|
|||
Maintainer |
|||
========== |
|||
.. 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/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Aysha Shalin (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC |
|||
# LICENSE (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
from . import models |
@ -0,0 +1,57 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Aysha Shalin (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC |
|||
# LICENSE (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
{ |
|||
'name': 'Product Management', |
|||
'version': '18.0.1.0.0', |
|||
'category': 'Extra Tools', |
|||
'summary': """Dashboard which displays top sales, purchases monthly |
|||
movements and location categorization of Products.""", |
|||
'description': """This module module for each product, accompanied by |
|||
its dedicated dashboard. This dashboard should feature charts highlighting |
|||
the top-selling products, top-purchased products, and monthly product |
|||
movements. Additionally, include a section displaying products categorized |
|||
by location.""", |
|||
'author': 'Cybrosys Techno Solutions', |
|||
'company': 'Cybrosys Techno Solutions', |
|||
'maintainer': 'Cybrosys Techno Solutions', |
|||
'website': "https://www.cybrosys.com", |
|||
'license': 'LGPL-3', |
|||
'depends': ['product', 'sale', 'purchase', 'stock'], |
|||
'data': [ |
|||
'views/product_views.xml', |
|||
'views/product_dashboard.xml', |
|||
], |
|||
'assets': { |
|||
'web.assets_backend': [ |
|||
'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js', |
|||
'product_management_app/static/src/css/dashboard_views.css', |
|||
'product_management_app/static/src/js/product_dashboard.js', |
|||
'product_management_app/static/src/xml/dashboard.xml', |
|||
], |
|||
}, |
|||
'images': [ |
|||
'static/description/banner.png', |
|||
], |
|||
'installable': True, |
|||
'auto_install': False, |
|||
'application': True |
|||
} |
@ -0,0 +1,6 @@ |
|||
## Module <product_management_app> |
|||
|
|||
#### 25.04.2025 |
|||
#### Version 18.0.1.0.0 |
|||
##### ADD |
|||
- Initial commit for Product Management |
@ -0,0 +1,23 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Aysha Shalin (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC |
|||
# LICENSE (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
from . import product |
|||
|
@ -0,0 +1,225 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Aysha Shalin (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC |
|||
# LICENSE (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
import calendar |
|||
from datetime import datetime |
|||
from collections import OrderedDict |
|||
from odoo import api, models |
|||
|
|||
|
|||
class ProductTemplate(models.Model): |
|||
""" The ProductTemplate model inherited to function for creating the |
|||
Template Dashboard values. """ |
|||
_inherit = 'product.template' |
|||
|
|||
@api.model |
|||
def get_data(self): |
|||
""" Return data to the tiles of dashboard """ |
|||
return { |
|||
'product_templates': self.search_count([]), |
|||
'product_variants': self.env['product.product'].search_count([]), |
|||
'combo': self.search_count([('type', '=', 'combo')]), |
|||
'goods': self.search_count([('type', '=', 'consu')]), |
|||
'service': self.search_count([('type', '=', 'service')]), |
|||
'category': self.env['product.category'].search_count([]), |
|||
'price_list': self.env['product.pricelist'].search_count([]), |
|||
'product_attribute': self.env['product.attribute'].search_count([]), |
|||
} |
|||
|
|||
@api.model |
|||
def get_top_sale_data(self): |
|||
""" Return the top sold products """ |
|||
query = ''' |
|||
select DISTINCT(product_template.name ->> 'en_US') as product_name, |
|||
sum(product_uom_qty) as total_quantity |
|||
from sale_order_line |
|||
inner join product_product |
|||
on product_product.id=sale_order_line.product_id |
|||
inner join product_template |
|||
on product_product.product_tmpl_id = product_template.id |
|||
where sale_order_line.company_id = ''' + str( |
|||
self.env.company.id) + ''' group by product_template.id |
|||
ORDER BY total_quantity DESC Limit 10 |
|||
''' |
|||
self._cr.execute(query) |
|||
top_product = self._cr.dictfetchall() |
|||
total_quantity = [record.get('total_quantity') for record in top_product] |
|||
product_name = [record.get('product_name') for record in top_product] |
|||
final = [total_quantity, product_name] |
|||
return final |
|||
|
|||
@api.model |
|||
def get_top_purchase_data(self): |
|||
""" Returns top purchased products """ |
|||
query = (''' |
|||
select DISTINCT(product_template.name ->> 'en_US') as product_name, |
|||
sum(product_qty) as total_quantity |
|||
from purchase_order_line |
|||
inner join product_product |
|||
on product_product.id=purchase_order_line.product_id |
|||
inner join product_template |
|||
on product_product.product_tmpl_id = product_template.id |
|||
where purchase_order_line.company_id = ''' + str( |
|||
self.env.company.id) + ''' |
|||
group by product_template.id |
|||
ORDER BY total_quantity DESC Limit 10 ''') |
|||
self._cr.execute(query) |
|||
top_product = self._cr.dictfetchall() |
|||
final = [ |
|||
[record.get('total_quantity') for record in top_product], |
|||
[record.get('product_name') for record in top_product] |
|||
] |
|||
return final |
|||
|
|||
@api.model |
|||
def get_product_location_analysis(self): |
|||
""" Returns the location and location id to the selection """ |
|||
categ_qry = """select id, complete_name from stock_location""" |
|||
self._cr.execute(categ_qry) |
|||
location = self._cr.dictfetchall() |
|||
location_id = [rec['id'] for rec in location] |
|||
location_name = [rec['complete_name'] for rec in location] |
|||
value1 = {'location_id': location_id, 'location_name': location_name} |
|||
return value1 |
|||
|
|||
@api.model |
|||
def get_products(self): |
|||
""" Returns the product and product name to the selection """ |
|||
data = self.env['product.template'].search([]) |
|||
product_id = [record['id'] for record in data] |
|||
product_name = [self.env['product.template'].search( |
|||
[('id', '=', record['id'])]).name for record in data] |
|||
value1 = {'product_id': product_id, 'product_name': product_name} |
|||
return value1 |
|||
|
|||
@api.model |
|||
def get_years(self): |
|||
""" To get the last 5 years """ |
|||
current_year = datetime.now().year |
|||
last_five_years = [current_year - year for year in range(5)] |
|||
return last_five_years |
|||
|
|||
@api.model |
|||
def get_prod_details(self, data, year): |
|||
""" Returns the monthly analysis of product movement """ |
|||
query = """ |
|||
select product_template.name as name,sum(stock_move_line.quantity), |
|||
stock_move_line.date as date_part |
|||
from stock_move_line |
|||
inner join product_product |
|||
on product_product.id = stock_move_line.product_id |
|||
inner join product_template |
|||
on product_product.product_tmpl_id = product_template.id |
|||
where stock_move_line.company_id = %s and product_template.id = %s |
|||
group by product_template.name, stock_move_line.date |
|||
""" % (self.env.company.id, data) |
|||
self._cr.execute(query) |
|||
product_moves = self._cr.dictfetchall() |
|||
product_move = [move for move in product_moves if |
|||
str(move['date_part'].year) == year] |
|||
month = [int(rec['date_part'].month) for rec in product_move] |
|||
for rec in product_move: |
|||
date_part = rec.get('date_part') |
|||
rec.update({ |
|||
'count': rec['sum'], |
|||
'dates': calendar.month_name[int(date_part.month)], |
|||
'month': int(date_part.month) |
|||
}) |
|||
[product_move.append({ |
|||
'count': 0, |
|||
'dates': calendar.month_name[rec], |
|||
'month': rec |
|||
}) for rec in range(1, 13) if rec not in month] |
|||
cr = sorted(product_move, key=lambda i: i['month']) |
|||
month_of_num = 0 |
|||
total_count = 0 |
|||
for rec in cr: |
|||
if month_of_num == rec['month']: |
|||
total_count += rec['count'] |
|||
if rec['count'] > 0: |
|||
rec.update({'count': total_count}) |
|||
else: |
|||
month_of_num = rec['month'] |
|||
total_count = rec['count'] |
|||
# OrderedDict to maintain insertion order |
|||
result = OrderedDict() |
|||
for item in cr: |
|||
# Check if month already exists |
|||
month = item['month'] |
|||
if month not in result or item['count'] > result[month]['count']: |
|||
result[month] = item |
|||
# Convert back to list |
|||
result = list(result.values()) |
|||
count = [rec['count'] for rec in result] |
|||
months = [rec['dates'] for rec in result] |
|||
return { |
|||
'count': count, |
|||
'dates': months |
|||
} |
|||
|
|||
@api.model |
|||
def product_move_by_category(self, args): |
|||
""" Rpc method of product moves by category. Returns category name and |
|||
quantity_done. """ |
|||
category_id = int(args) |
|||
company_id = self.env.company.id |
|||
query = (''' |
|||
select product_template.name,sum(stock_move_line.qty_done) |
|||
from stock_move_line |
|||
inner join product_product |
|||
on stock_move_line.product_id = product_product.id |
|||
inner join product_template |
|||
on product_product.product_tmpl_id = product_template.id |
|||
inner join product_category |
|||
on product_template.categ_id = product_category.id |
|||
where stock_move_line.company_id = %s and product_category.id = %s |
|||
group by product_template.name |
|||
''' % (company_id, category_id)) |
|||
self._cr.execute(query) |
|||
product_move = self._cr.dictfetchall() |
|||
quantity_done = [record['sum'] for record in product_move] |
|||
name = [record['name'] for record in product_move] |
|||
value = { |
|||
'name': name, |
|||
'count': quantity_done, |
|||
} |
|||
return value |
|||
|
|||
@api.model |
|||
def get_product_qty_by_loc(self, args): |
|||
""" Returns product qty based on the location selected. """ |
|||
query = (''' |
|||
select sl.complete_name, pt.name as name, sq.quantity |
|||
from stock_quant sq |
|||
inner join stock_location sl on sq.location_id = sl.id |
|||
inner join product_product pp on sq.product_id = pp.id |
|||
inner join product_template pt on pp.product_tmpl_id = pt.id |
|||
where sq.company_id = '%s' and sl.id = '%s' |
|||
group by sl.complete_name,pt.name, sq.quantity |
|||
''' % (self.env.company.id, int(args))) |
|||
self._cr.execute(query) |
|||
product_qty = self._cr.dictfetchall() |
|||
product = [rec['name'] for rec in product_qty] |
|||
quantity = [rec['quantity'] for rec in product_qty] |
|||
return { |
|||
'products': product, |
|||
'quantity': quantity, |
|||
} |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 628 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 210 KiB |
After Width: | Height: | Size: 209 KiB |
After Width: | Height: | Size: 109 KiB |
After Width: | Height: | Size: 495 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 624 B |
After Width: | Height: | Size: 136 KiB |
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 929 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 542 B |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 738 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 600 B |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 462 B |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 926 B |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 800 B |
After Width: | Height: | Size: 905 B |
After Width: | Height: | Size: 189 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 875 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 757 KiB |
After Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 126 KiB |
After Width: | Height: | Size: 146 KiB |
After Width: | Height: | Size: 83 KiB |
After Width: | Height: | Size: 388 KiB |
After Width: | Height: | Size: 111 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 880 KiB |
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 11 KiB |
@ -0,0 +1,320 @@ |
|||
.oh_dashboards{ |
|||
padding-top :15px; |
|||
background-color: #f8faff !important; |
|||
} |
|||
|
|||
.oh-card h4 { |
|||
font-size: 1.1rem; |
|||
} |
|||
|
|||
/* Widget One |
|||
---------------------------*/ |
|||
.stat-content { |
|||
display: inline-block; |
|||
width: 66%; |
|||
} |
|||
.stat-icon{ |
|||
display: inline-block; |
|||
} |
|||
|
|||
.stat-widget-one .stat-icon { |
|||
vertical-align: top; |
|||
margin: auto; |
|||
width: 100%; |
|||
color: #01c490; |
|||
} |
|||
|
|||
.stat-widget-one .stat-icon i { |
|||
font-size: 30px; |
|||
font-weight: 900; |
|||
display: inline-block; |
|||
color: #01c490;} |
|||
|
|||
.stat-widget-one .stat-text { |
|||
font-size: 14px; |
|||
color: #868e96; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.stat-widget-one .stat-digit { |
|||
font-size: 24px; |
|||
color: #02448b; } |
|||
|
|||
.stat-count { |
|||
font-size: 20px; |
|||
text-align: center; |
|||
color: #00438b;} |
|||
|
|||
.stat-title { |
|||
font-size: 17px; |
|||
text-align: center; |
|||
color: #00438b; } |
|||
|
|||
.mb-0{ |
|||
position: relative; |
|||
} |
|||
.mb-0 .dash-title { |
|||
font-size: 20px; |
|||
color: rgba(255, 255, 255, 0.81); |
|||
} |
|||
|
|||
.oh-card { |
|||
|
|||
padding-top: 0px; |
|||
padding: 0px; |
|||
margin-bottom: 1.5rem; |
|||
border-radius: 0px; |
|||
box-shadow: none; |
|||
background: none; |
|||
transition: transform 0.2s ease, box-shadow 0.2s ease; |
|||
will-change: transform, box-shadow; |
|||
|
|||
} |
|||
.oh-card:hover { |
|||
|
|||
transform: translateY(-2px) translateZ(0) !important; |
|||
box-shadow: 0 10px 10px 0 rgba(62, 57, 107, 0.12), 0 0 0 transparent !important; |
|||
|
|||
} |
|||
.oh-data { |
|||
margin-top: 2.5%; |
|||
} |
|||
.oh-data .stat-icon { |
|||
width: 30%; |
|||
height: 85px; |
|||
text-align: center; |
|||
background: #ff8762; |
|||
color: #fff; |
|||
width: 32%; |
|||
padding-top: 2%; |
|||
font-size: xxx-large; |
|||
|
|||
} |
|||
.oh-data .oh-card { |
|||
|
|||
transition: transform 0.2s ease, box-shadow 0.2s ease; |
|||
will-change: transform, box-shadow; |
|||
box-shadow: 0 10px 40px 0 rgba(62,57,107,0.07), 0 2px 9px 0 rgba(62,57,107,0.06); |
|||
|
|||
} |
|||
.stat-widget-one .stat-text { |
|||
font-size: 14px; |
|||
color: #ff8762; |
|||
margin-top: 2.3rem; |
|||
margin-left: 1rem; |
|||
} |
|||
.stat-widget-one .stat-digit { |
|||
font-size: 26px; |
|||
color:#993232; |
|||
margin-left: 1rem; |
|||
margin-top: -1px; |
|||
font-family: initial |
|||
} |
|||
|
|||
.stat-widget-one .stat-icon i { |
|||
|
|||
font-size: 25px; |
|||
font-weight: 900; |
|||
display: inline-block; |
|||
color: #fff; |
|||
|
|||
} |
|||
.stat-widget-one { |
|||
|
|||
background-color: white; |
|||
text-align: left; |
|||
|
|||
} |
|||
.stat-widget-one { |
|||
width: 100%; |
|||
} |
|||
.oh-data .stat-icon { |
|||
|
|||
width: 30%; |
|||
height: 85px; |
|||
text-align: center; |
|||
padding-top: 2%; |
|||
|
|||
} |
|||
|
|||
h4 .stat-count { |
|||
font-size: 17px; |
|||
text-align: center; |
|||
color: #000 !important; |
|||
margin-top: 0px; |
|||
width: 100%; |
|||
float: left; |
|||
margin: 0; |
|||
} |
|||
|
|||
.stat-head { |
|||
text-align: left !important; |
|||
font-weight: 300; |
|||
font-size: 15px; |
|||
margin-bottom: 25px; |
|||
margin-left: 24px; |
|||
width: 100%; |
|||
} |
|||
.oh-card-body { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
} |
|||
|
|||
.o_action_manager{ |
|||
overflow-y: scroll !important; |
|||
max-width:100%; |
|||
} |
|||
|
|||
.stat_count{ |
|||
margin-top: -89px; |
|||
margin-left: 35px; |
|||
font-size: 33px; |
|||
} |
|||
|
|||
|
|||
.stat-head { |
|||
text-align: left !important; |
|||
font-weight: 300; |
|||
font-size: 18px; |
|||
margin-bottom: 25px; |
|||
margin-left: 24px; |
|||
width: 100%; |
|||
margin-top: 57px; |
|||
color: black; |
|||
} |
|||
|
|||
.top_chart{ |
|||
height: fit-content; |
|||
} |
|||
.row.main-section { |
|||
margin-right: 0px; !important; |
|||
} |
|||
|
|||
.chart-container { |
|||
border-radius: 0.3rem; |
|||
padding: 1rem; |
|||
margin: 1rem auto; |
|||
} |
|||
|
|||
.chart-container.card-shadow { |
|||
height: 100%; |
|||
} |
|||
|
|||
.half_chart.chart-container.card-shadow { |
|||
height: 49%; |
|||
} |
|||
#container { |
|||
height: 400px; |
|||
} |
|||
|
|||
|
|||
/*table*/ |
|||
.graph_details_table{ |
|||
position: absolute; |
|||
top: 45px; |
|||
right: 15px; |
|||
|
|||
background-color: white; |
|||
border-collapse: collapse; |
|||
border: 1px solid #ddd; |
|||
|
|||
} |
|||
|
|||
.graph_details_table th{ |
|||
background-color:#67b7dc; |
|||
color: white; |
|||
width: 250px; |
|||
height: 30px; |
|||
} |
|||
|
|||
.graph_details_table td{ |
|||
border: 1px solid #ddd; |
|||
height: 20px; |
|||
} |
|||
|
|||
.graph_details_table tr:nth-child(even){background-color: #f2f2f2;} |
|||
|
|||
.graph_details_table tr:hover {background-color: #ddd;} |
|||
|
|||
.chart-container h2 { |
|||
font-weight: 700; |
|||
font-size: 1.690rem; |
|||
} |
|||
|
|||
.hr_notification { |
|||
background: #f6f7fa; |
|||
transition: transform 0.2s ease, box-shadow 0.2s ease; |
|||
will-change: transform, box-shadow; |
|||
box-shadow: 0 10px 40px 0 rgba(62,57,107,0.07), 0 2px 9px 0 rgba(62,57,107,0.06); |
|||
height: 316px; |
|||
overflow-y: auto; |
|||
margin-bottom: 15px; |
|||
} |
|||
.hr_notification .media { |
|||
border-bottom: 1px solid #e6e6e6; |
|||
padding-bottom: 6px; |
|||
margin-bottom: 10px; |
|||
} |
|||
hr_notification .text-color.display-6 { |
|||
margin: 0px 0 3px; |
|||
color: #2d2d2d; |
|||
} |
|||
.hr_notification p { |
|||
margin: 0 0 1px; |
|||
color: #666; |
|||
font-size: 10px; |
|||
} |
|||
.chart_head { |
|||
width: 850px; |
|||
margin-left: 21px; |
|||
font-size: 17px; |
|||
text-align: center; |
|||
padding: 12px 0; |
|||
color: #fff; |
|||
margin-bottom: 9px; |
|||
} |
|||
.hr_notification_head { |
|||
font-size: 17px; |
|||
text-align: center; |
|||
padding: 12px 0; |
|||
color: #fff; |
|||
font-weight: 300; |
|||
background: #5ebade; |
|||
margin-bottom: 9px; |
|||
} |
|||
@media (max-width: 767.98px) { |
|||
.oh-card{ |
|||
width: 100% !important; |
|||
} |
|||
.dashboard_main_section .form-control{ |
|||
width: 85% !important; |
|||
margin: 15px auto; |
|||
} |
|||
.sale_details{ |
|||
position: relative; |
|||
top: 0px !important; |
|||
right: 0px !important; |
|||
} |
|||
.selling_product_graph_view{ |
|||
margin-top: 5rem; |
|||
} |
|||
.hr_notification{ |
|||
min-width: 90% !important; |
|||
} |
|||
} |
|||
.hr_notification{ |
|||
min-width: 425.683px; |
|||
min-height: 316px; |
|||
max-width: 100%; |
|||
} |
|||
.media-body{ |
|||
|
|||
background: #466b8d; |
|||
float: left; |
|||
margin: 0; |
|||
width: 100% |
|||
} |
|||
.graph_canvas{ |
|||
height: 350px; |
|||
} |
@ -0,0 +1,357 @@ |
|||
/** @odoo-module **/ |
|||
import { registry } from "@web/core/registry"; |
|||
import { _t } from "@web/core/l10n/translation"; |
|||
import { Component, onWillStart, onMounted, useState, useRef } from "@odoo/owl"; |
|||
import { useService } from "@web/core/utils/hooks"; |
|||
export class ProductDashboard extends Component{ |
|||
setup(){ |
|||
super.setup(...arguments); |
|||
this.orm = useService("orm"); |
|||
this.TopSaleChart = useRef("top_sale_chart") |
|||
this.TopPurchaseChart = useRef("top_purchase_chart") |
|||
this.ProductChart = useRef("product_graph") |
|||
this.ProductQtyChart = useRef("product_qty") |
|||
this.YearSelection = useRef("year-selection") |
|||
this.ProductSelection = useRef("product-selection") |
|||
this.ProductLocation = useRef("product-location") |
|||
this.state = useState({ |
|||
product_templates : [], |
|||
variants_count : [], |
|||
products_storable : [], |
|||
product_consumable: [], |
|||
product_service: [], |
|||
product_pricelist : [], |
|||
product_attribute:[], |
|||
location_chart: [], |
|||
move_chart: [], |
|||
}); |
|||
onWillStart(async () => { |
|||
await this.fetch_data(); |
|||
}); |
|||
onMounted(async ()=> { |
|||
await this.render_top_sold_product(); |
|||
await this.render_top_purchase_product(); |
|||
await this.render_year_chart (); |
|||
await this.render_monthly_chart (); |
|||
await this.onchange_prod_selection(); |
|||
await this.render_product_categ_analysis(); |
|||
await this.onchange_location_selection(); |
|||
}); |
|||
} |
|||
async fetch_data() { |
|||
// Fetch data to the tiles
|
|||
var self = this |
|||
var result = await this.orm.call( 'product.template', "get_data",[]) |
|||
this.state.product_templates = result['product_templates'] |
|||
this.state.variants_count = result['product_variants'] |
|||
this.state.products_storable = result['combo'] |
|||
this.state.product_consumable = result['goods'] |
|||
this.state.product_service = result['service'] |
|||
this.state.product_categ = result['category'] |
|||
this.state.product_pricelist = result['price_list'] |
|||
this.state.product_attribute = result['product_attribute'] |
|||
} |
|||
async render_top_sold_product() { |
|||
// To view the top sale products in the chart
|
|||
var self = this |
|||
var ctx = this.TopSaleChart.el; |
|||
const arrays = await this.orm.call( |
|||
'product.template', "get_top_sale_data",[]) |
|||
var data = { |
|||
labels : arrays[1], |
|||
datasets: [{ |
|||
label: "", |
|||
data: arrays[0], |
|||
backgroundColor: [ |
|||
"#1E90FF", |
|||
"#95B9C7", |
|||
"#66CDAA", |
|||
"#FF7F50", |
|||
"#F67280", |
|||
"#810541", |
|||
"#7D0552", |
|||
"#D58A94", |
|||
"#B041FF" |
|||
], |
|||
borderColor: [ |
|||
"#1E90FF", |
|||
"#95B9C7", |
|||
"#66CDAA", |
|||
"#FF7F50", |
|||
"#F67280", |
|||
"#810541", |
|||
"#7D0552", |
|||
"#D58A94", |
|||
"#B041FF" |
|||
], |
|||
borderWidth: 1 |
|||
},] |
|||
}; |
|||
//options
|
|||
var options = { |
|||
responsive: true, |
|||
title: false, |
|||
legend: { |
|||
display: true, |
|||
position: "right", |
|||
labels: { |
|||
fontColor: "#333", |
|||
fontSize: 16 |
|||
} |
|||
}, |
|||
scales: { |
|||
yAxes: [{ |
|||
gridLines: { |
|||
color: "rgba(0, 0, 0, 0)", |
|||
display: false, |
|||
}, |
|||
ticks: { |
|||
min: 0, |
|||
display: false, |
|||
} |
|||
}] |
|||
} |
|||
}; |
|||
//create Chart class object
|
|||
var chart = new Chart(ctx, { |
|||
type: "pie", |
|||
data: data, |
|||
options: options |
|||
}); |
|||
} |
|||
async render_top_purchase_product() { |
|||
// To view the top purchase products in the chart
|
|||
var self = this |
|||
var ctx = this.TopPurchaseChart.el; |
|||
const arrays = await this.orm.call( 'product.template', "get_top_purchase_data",[]) |
|||
var data = { |
|||
labels : arrays[1], |
|||
datasets: [{ |
|||
label: "", |
|||
data: arrays[0], |
|||
backgroundColor: [ |
|||
"#003f5c", |
|||
"#2f4b7c", |
|||
"#f95d6a", |
|||
"#665191", |
|||
"#d45087", |
|||
"#ff7c43", |
|||
"#ffa600", |
|||
"#a05195", |
|||
"#6d5c16" |
|||
], |
|||
borderColor: [ |
|||
"#003f5c", |
|||
"#2f4b7c", |
|||
"#f95d6a", |
|||
"#665191", |
|||
"#d45087", |
|||
"#ff7c43", |
|||
"#ffa600", |
|||
"#a05195", |
|||
"#6d5c16" |
|||
], |
|||
borderWidth: 1 |
|||
},] |
|||
}; |
|||
//options
|
|||
var options = { |
|||
responsive: true, |
|||
title: false, |
|||
legend: { |
|||
display: true, |
|||
position: "right", |
|||
labels: { |
|||
fontColor: "#333", |
|||
fontSize: 16 |
|||
} |
|||
}, |
|||
scales: { |
|||
yAxes: [{ |
|||
gridLines: { |
|||
color: "rgba(0, 0, 0, 0)", |
|||
display: false, |
|||
}, |
|||
ticks: { |
|||
min: 0, |
|||
display: false, |
|||
} |
|||
}] |
|||
} |
|||
}; |
|||
//create Chart class object
|
|||
var chart = new Chart(ctx, { |
|||
type: "doughnut", |
|||
data: data, |
|||
options: options |
|||
}); |
|||
} |
|||
async render_year_chart() { |
|||
// For adding the last 5 years in the selection field
|
|||
var self = this |
|||
const data = await this.orm.call( 'product.template', "get_years",[]) |
|||
const select = this.YearSelection.el || this.YearSelection.getEl?.(); |
|||
if (select) { |
|||
data.forEach(year => { |
|||
const option = document.createElement("option"); |
|||
option.value = year; |
|||
option.textContent = year; |
|||
select.appendChild(option); // Use appendChild to append the option to the select
|
|||
}); |
|||
} else { |
|||
console.error("Year selection element not found."); |
|||
} |
|||
} |
|||
async render_monthly_chart() { |
|||
// For listing the total products to filter the chart based on products
|
|||
var self = this |
|||
const data = await this.orm.call( 'product.template', "get_products",[]) |
|||
const products = this.ProductSelection.el || this.ProductSelection.getEl?.(); |
|||
// Ensure the product selection element is valid
|
|||
if (products) { |
|||
products.innerHTML = ''; |
|||
var k = 0; |
|||
// Loop through the product names and append options
|
|||
Object.entries(data.product_name).forEach(([key, value]) => { |
|||
const option = document.createElement("option"); |
|||
option.value = data.product_id[k]; |
|||
option.textContent = value; |
|||
if (k == 0) { |
|||
option.selected = true; |
|||
} |
|||
products.appendChild(option); |
|||
k++; |
|||
}); |
|||
} else { |
|||
console.error("Product selection element not found or is not a <select> element."); |
|||
} |
|||
} |
|||
async onchange_prod_selection() { |
|||
/* The filter is based on changes in products, displaying monthly |
|||
product movements*/ |
|||
if (this.state.move_chart.length != 0) { |
|||
this.state.move_chart.forEach((item)=> { |
|||
item.destroy() |
|||
}); |
|||
} |
|||
var productElement = this.ProductSelection.el || this.ProductSelection.getEl?.(); |
|||
var option = productElement ? productElement.value : null; |
|||
var yearElement = this.YearSelection.el || this.YearSelection.getEl?.(); |
|||
var year = yearElement ? yearElement.value : null; |
|||
var ctx = this.ProductChart.el; |
|||
const result = await this.orm.call('product.template', "get_prod_details", [option, year]) |
|||
var name = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] |
|||
var data = { |
|||
labels: name,//x axis
|
|||
datasets: [{ |
|||
label: 'Purchase Order Total', // Name the series
|
|||
data: result.count, // Specify the data values array
|
|||
backgroundColor: '#0000ff', |
|||
borderColor: '#0000ff', |
|||
barPercentage: 0.5, |
|||
barThickness: 6, |
|||
maxBarThickness: 8, |
|||
minBarLength: 0, |
|||
borderWidth: 1, // Specify bar border width
|
|||
type: 'line', // Set this data to a line chart
|
|||
fill: false |
|||
},] |
|||
} |
|||
var options = { |
|||
scales: { |
|||
y: { |
|||
beginAtZero: true |
|||
}, |
|||
}, |
|||
responsive: true, // Instruct chart js to respond nicely.
|
|||
maintainAspectRatio: false, // Add to prevent default behaviour of full-width/height
|
|||
} |
|||
var chart = new Chart(ctx, { |
|||
type: "line", |
|||
data: data, |
|||
options: options |
|||
}); |
|||
this.state.move_chart.push(chart) |
|||
} |
|||
async render_product_categ_analysis() { |
|||
/* For listing the whole locations in the inventory in the*/ |
|||
var self = this |
|||
const data = await this.orm.call( 'product.template', "get_product_location_analysis",[]) |
|||
var location = this.ProductLocation.el || this.ProductLocation.getEl?.(); |
|||
if (location) { |
|||
location.innerHTML = ''; |
|||
var k = 0; |
|||
// Loop through the product names and append options
|
|||
Object.entries(data.location_name).forEach(([key, value]) => { |
|||
const option = document.createElement("option"); |
|||
option.id = key |
|||
option.value = data.location_id[k]; |
|||
option.textContent = value; |
|||
if (k == 0) { |
|||
option.selected = true; |
|||
} |
|||
location.appendChild(option); |
|||
k++; |
|||
}); |
|||
} |
|||
|
|||
// Object.entries(data.location_name).forEach(([key, value]) => {
|
|||
// if(k == 0){
|
|||
// $('#product_location_selection').append('<option id="'+key+'" value="'+data.location_id[k]+'" selected="selected">'+value+'</option>')
|
|||
// k++;
|
|||
// }else{
|
|||
// $('#product_location_selection').append('<option id="'+key+'" value="'+data.location_id[k]+'">'+value+'</option>')
|
|||
// k++;
|
|||
// }
|
|||
// });
|
|||
} |
|||
async onchange_location_selection() { |
|||
/* The filter is based on changes in location, products based on the location */ |
|||
if (this.state.location_chart.length != 0) { |
|||
this.state.location_chart.forEach((item)=> { |
|||
item.destroy() |
|||
}); |
|||
} |
|||
var locationElement = this.ProductLocation.el || this.ProductSelection.getEl?.(); |
|||
var option = locationElement ? locationElement.value : null; |
|||
var ctx = this.ProductQtyChart.el; |
|||
const result = await this.orm.call('product.template', "get_product_qty_by_loc", [option]) |
|||
var product_list = [] |
|||
for (var product in result.products) { |
|||
product_list.push(result.products[product]['en_US']) |
|||
} |
|||
var data = { |
|||
labels: product_list, |
|||
datasets: [{ |
|||
label: 'Count', // Name the series
|
|||
data: result.quantity, // Specify the data values array
|
|||
backgroundColor: '#ac3973', |
|||
borderColor: '#ac3973', |
|||
barPercentage: 0.5, |
|||
barThickness: 6, |
|||
maxBarThickness: 8, |
|||
minBarLength: 0, |
|||
borderWidth: 1, // Specify bar border width
|
|||
type: 'bar', // Set this data to a line chart
|
|||
fill: false |
|||
}] |
|||
} |
|||
var options = { |
|||
scales: { |
|||
y: { |
|||
beginAtZero: true |
|||
}, |
|||
}, |
|||
responsive: true, // Instruct chart js to respond nicely.
|
|||
maintainAspectRatio: false, // Add to prevent default behaviour of full-width/height
|
|||
} |
|||
var chart = new Chart(ctx, { |
|||
type: "bar", |
|||
data: data, |
|||
options: options |
|||
}); |
|||
this.state.location_chart.push(chart) |
|||
} |
|||
} |
|||
ProductDashboard.template = 'ProductDashboard' |
|||
registry.category("actions").add("product_dashboard_tag", ProductDashboard) |
@ -0,0 +1,237 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<template> |
|||
<t t-name="ProductDashboard"> |
|||
<div class="oh_dashboards"> |
|||
</div> |
|||
<section class="dashboard_main" id="main_section"> |
|||
<div class="col-sm-12 mb-4"> |
|||
<div class="row" style="margin-left: 20px"> |
|||
<div class="shadow-sm border m-2 p-4" style="height: 30px; width: 97.5%; border-radius: 10px; background: lavender"> |
|||
<div class="d-flex justify-content-between"> |
|||
<p class="section-header fw-bold" style="margin-top: -16px; font-size: 22px;">PRODUCT DASHBOARD</p> |
|||
</div> |
|||
</div> |
|||
<!-- Tiles --> |
|||
<div class="col-sm-12 mb-3" > |
|||
<div class="row" style="margin-left: 38px"> |
|||
<div class="col-md-3 col-sm-5 all_products oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#ff4d94"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Products</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.product_templates"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-5 product_variants oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#99e6ff"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Product Variants</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.variants_count"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-5 storable_products oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#aaff80"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Combo Products</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.products_storable"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-5 consu_products oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#bf80ff"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Goods Products</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.product_consumable"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-5 service_prod oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#ff4d94"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Service Products</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.product_service"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-5 categ_prod oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#99e6ff"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Product Category</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.product_categ"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-5 categ_prod oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#aaff80"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Price Lists</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.product_pricelist"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-5 categ_prod oh-data"> |
|||
<div class="oh-card" style="width: 288px;"> |
|||
<div class="oh-card-body"> |
|||
<div class="stat-widget-one"> |
|||
<div class="stat-icon" style="background:#bf80ff"> |
|||
<i class="fa fa-star"/> |
|||
</div> |
|||
<div class="stat-content"> |
|||
<div class="stat-head">Product Attribute</div> |
|||
<div class="stat_count"> |
|||
<t t-esc="this.state.product_attribute"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<div class="col-xs-12 col-sm-12 col-lg-12 col-md-12"> |
|||
<div class="row main-section"> |
|||
<div class="col-md-6"> |
|||
<div class="chart_head" |
|||
style="font-weight: 700;background: #0b3c38b0;"> |
|||
Top Sale Charts |
|||
</div> |
|||
<div class="d-flex justify-content-center shadow-sm border m-4 p-4 bg-white"> |
|||
<div style="height:500px; width: 600px;"> |
|||
<canvas t-ref="top_sale_chart" width="500" height="500"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<div class="chart_head" |
|||
style="font-weight: 700;background: #3e6c1eb0;"> |
|||
Top Purchase Charts |
|||
</div> |
|||
<div class="d-flex justify-content-center shadow-sm border m-4 p-4 bg-white"> |
|||
<div style="height:500px; width: 600px;"> |
|||
<canvas t-ref="top_purchase_chart"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<br/> |
|||
<div class="col-xs-12 col-sm-12 col-lg-12 col-md-12"> |
|||
<div class="row main-section"> |
|||
<div class="col-sm-6 col-lg-6"> |
|||
<div class="chart_head" |
|||
style="font-weight: 700;background: #669731d4;"> |
|||
Product Moves Monthly Analysis |
|||
</div> |
|||
<div class="shadow-sm border m-4 p-4 bg-white" style="height: 636px;"> |
|||
<div class="row" style="margin-left: 5px;"> |
|||
<!-- Product Select --> |
|||
<div class="form-group col-4" |
|||
style="padding: 12px 0;" id="product_select"> |
|||
<select class="form-select" id="prod_selection" |
|||
t-on-change="onchange_prod_selection" |
|||
style="background-color: ghostwhite;" t-ref="product-selection"> |
|||
</select> |
|||
</div> |
|||
<!-- Year Select --> |
|||
<div class="form-group col-2" |
|||
style="padding: 12px 0; margin-left: 5px;" id="year_select"> |
|||
<select class="form-select" id="year_selection" |
|||
style="background-color: ghostwhite;" t-ref="year-selection"> |
|||
</select> |
|||
</div> |
|||
</div> |
|||
<div class="graph_canvas" style="margin-top: 30px;"> |
|||
<canvas t-ref="product_graph"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-sm-6 col-lg-6"> |
|||
<div class="chart_head" |
|||
style="font-weight: 700;background: #137660c2;"> |
|||
Product By Location |
|||
</div> |
|||
<div class="shadow-sm border m-4 p-4 bg-white" style="height: 636px;"> |
|||
<div class="form-group col-4" |
|||
style="padding: 12px 0;" id="product_categ_select"> |
|||
<select class="form-select" id="product_location_selection" |
|||
t-on-change="onchange_location_selection" |
|||
style="background-color: ghostwhite;" t-ref="product-location"> |
|||
</select> |
|||
</div> |
|||
<div class="graph_canvas" style="margin-top: 30px;"> |
|||
<canvas t-ref="product_qty"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<br/> |
|||
</t> |
|||
</template> |
@ -0,0 +1,12 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<record id="product_dashboard_action" model="ir.actions.client"> |
|||
<field name="name">Dashboard</field> |
|||
<field name="tag">product_dashboard_tag</field> |
|||
</record> |
|||
<menuitem id="dashboard" |
|||
name="Dashboard" |
|||
parent="product_management_app.menu_product_root" |
|||
action="product_dashboard_action" |
|||
sequence="-10"/> |
|||
</odoo> |
@ -0,0 +1,23 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Added Menus --> |
|||
<menuitem id="menu_product_root" name="Products" |
|||
web_icon="product_management_app,static/description/icon.png" |
|||
sequence="10"/> |
|||
<menuitem id="menu_product_template" name="Products" parent="menu_product_root" |
|||
action="product.product_template_action"/> |
|||
<menuitem id="menu_product_variants" name="Products Variants" |
|||
parent="menu_product_root" action="product.product_normal_action" |
|||
groups="product.group_product_variant" sequence="20"/> |
|||
<menuitem id="menu_product_pricelist" name="Pricelists" |
|||
parent="menu_product_root" |
|||
action="product.product_pricelist_action2" |
|||
groups="product.group_product_pricelist" sequence="30"/> |
|||
<menuitem id="menu_product_stock" name="Stock" parent="menu_product_root" |
|||
sequence="230"/> |
|||
<menuitem action="stock.stock_move_action" id="stock_move_menu" |
|||
parent="menu_product_stock" groups="base.group_no_one"/> |
|||
<menuitem action="stock.stock_move_line_action" |
|||
id="stock_move_line_menu" |
|||
parent="menu_product_stock" sequence="200"/> |
|||
</odoo> |