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