@ -0,0 +1,47 @@ |
|||||
|
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg |
||||
|
:target: https://www.gnu.org/licenses/agpl-3.0-standalone.html |
||||
|
:alt: License: AGPL-3 |
||||
|
|
||||
|
CRM Dashboard |
||||
|
============== |
||||
|
* Visual report of CRM through Dashboard. |
||||
|
|
||||
|
Configuration |
||||
|
============= |
||||
|
* No additional configurations needed |
||||
|
|
||||
|
Company |
||||
|
------- |
||||
|
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
||||
|
|
||||
|
License |
||||
|
------- |
||||
|
Affero General Public License, Version 3 (AGPL v3). |
||||
|
(https://www.gnu.org/licenses/agpl-3.0-standalone.html) |
||||
|
|
||||
|
Credits |
||||
|
------- |
||||
|
Developer: (V17) Mruthul Raj, |
||||
|
(V18) Raneesha, |
||||
|
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: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
from . import models |
@ -0,0 +1,53 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################################ |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
{ |
||||
|
'name': "CRM Dashboard", |
||||
|
'version': '18.0.1.0.0', |
||||
|
'category': 'Extra Tools', |
||||
|
'summary': """Get a visual report of CRM through a Dashboard in CRM """, |
||||
|
'description': """CRM dashboard module brings a multipurpose graphical |
||||
|
dashboard for CRM module and making the relationship management |
||||
|
better and easier""", |
||||
|
'author': 'Cybrosys Techno Solutions', |
||||
|
'company': 'Cybrosys Techno Solutions', |
||||
|
'maintainer': 'Cybrosys Techno Solutions', |
||||
|
'website': "https://www.cybrosys.com", |
||||
|
'depends': ['crm', 'sale_management'], |
||||
|
'data': ['views/crm_dashboard_views.xml', |
||||
|
'views/res_users_views.xml', |
||||
|
'views/utm_campaign_views.xml', |
||||
|
'views/crm_team_views.xml', |
||||
|
], |
||||
|
'assets': { |
||||
|
'web.assets_backend': [ |
||||
|
'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js', |
||||
|
'crm_dashboard/static/src/css/style.css', |
||||
|
'crm_dashboard/static/src/js/crm_dashboard.js', |
||||
|
'crm_dashboard/static/src/xml/dashboard_templates.xml', |
||||
|
], |
||||
|
}, |
||||
|
'images': ['static/description/banner.png'], |
||||
|
'license': 'AGPL-3', |
||||
|
'installable': True, |
||||
|
'application': False, |
||||
|
'auto_install': False, |
||||
|
} |
@ -0,0 +1,6 @@ |
|||||
|
## Module <crm_dashboard> |
||||
|
|
||||
|
#### 07.01.2025 |
||||
|
#### Version 18.0.1.0.0 |
||||
|
#### ADD |
||||
|
- Initial commit for CRM Dashboard |
@ -0,0 +1,26 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################################ |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
from . import crm_lead |
||||
|
from . import crm_team |
||||
|
from . import res_user |
||||
|
from . import sale_order |
||||
|
from . import utm_campaign |
@ -0,0 +1,438 @@ |
|||||
|
################################################################################ |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
import calendar |
||||
|
from dateutil.relativedelta import relativedelta |
||||
|
from odoo import api, fields, models |
||||
|
from odoo.tools.safe_eval import datetime |
||||
|
|
||||
|
|
||||
|
def get_period_start_date(period): |
||||
|
"""Returns the start date for the given period""" |
||||
|
today = datetime.datetime.now() |
||||
|
|
||||
|
if period == 'month': |
||||
|
start_date = today.replace(day=1) |
||||
|
elif period == 'quarter': |
||||
|
current_month = today.month |
||||
|
start_month = ((current_month - 1) // 3) * 3 + 1 |
||||
|
start_date = today.replace(month=start_month, day=1) |
||||
|
elif period == 'year': |
||||
|
start_date = today.replace(month=1, day=1) |
||||
|
elif period == 'week': |
||||
|
start_date = today - datetime.timedelta(days=today.weekday()) |
||||
|
else: |
||||
|
raise ValueError("Invalid period specified") |
||||
|
|
||||
|
return start_date.date() |
||||
|
|
||||
|
|
||||
|
class CRMLead(models.Model): |
||||
|
"""Extends crm.lead for adding more functions in it""" |
||||
|
_inherit = 'crm.lead' |
||||
|
|
||||
|
@api.model |
||||
|
def get_data(self, period): |
||||
|
"""Returns data to the dashboard tiles""" |
||||
|
period_days = get_period_start_date(period) |
||||
|
|
||||
|
crm_model = self.search([('create_date', '>=', period_days)]) |
||||
|
|
||||
|
lead_count = 0 |
||||
|
opportunity_count = 0 |
||||
|
win_count = 0 |
||||
|
active_lead_count = 0 |
||||
|
active_opportunity_count = 0 |
||||
|
won_opportunity_count = 0 |
||||
|
total_seconds = 0 |
||||
|
expected_revenue = 0 |
||||
|
revenue = 0 |
||||
|
unassigned_leads = 0 |
||||
|
|
||||
|
for record in crm_model: |
||||
|
if record.type == 'lead': |
||||
|
lead_count += 1 |
||||
|
if not record.user_id: |
||||
|
unassigned_leads += 1 |
||||
|
|
||||
|
if record.type == 'opportunity': |
||||
|
opportunity_count += 1 |
||||
|
expected_revenue += record.expected_revenue |
||||
|
if record.active: |
||||
|
if record.probability == 0: |
||||
|
active_opportunity_count += 1 |
||||
|
elif record.probability == 100: |
||||
|
won_opportunity_count += 1 |
||||
|
if record.stage_id.is_won: |
||||
|
revenue += record.expected_revenue |
||||
|
|
||||
|
if record.active: |
||||
|
if record.probability == 0: |
||||
|
active_lead_count += 1 |
||||
|
elif record.probability == 100: |
||||
|
win_count += 1 |
||||
|
|
||||
|
if record.date_conversion: |
||||
|
total_seconds += ( |
||||
|
record.date_conversion - record.create_date).seconds |
||||
|
|
||||
|
win_ratio = win_count / active_lead_count if active_lead_count else 0 |
||||
|
opportunity_ratio = won_opportunity_count / active_opportunity_count if active_opportunity_count else 0 |
||||
|
avg_close_time = round(total_seconds / len(crm_model.filtered( |
||||
|
lambda l: l.date_conversion))) if total_seconds else 0 |
||||
|
|
||||
|
return { |
||||
|
'leads': lead_count, |
||||
|
'opportunities': opportunity_count, |
||||
|
'exp_revenue': expected_revenue, |
||||
|
'revenue': revenue, |
||||
|
'win_ratio': win_ratio, |
||||
|
'opportunity_ratio': opportunity_ratio, |
||||
|
'avg_close_time': avg_close_time, |
||||
|
'unassigned_leads': unassigned_leads, |
||||
|
} |
||||
|
|
||||
|
@api.model |
||||
|
def get_lead_stage_data(self, period): |
||||
|
"""funnel chart""" |
||||
|
period_days = get_period_start_date(period) |
||||
|
crm_model = self.search([('create_date', '>=', period_days)]) |
||||
|
stage_lead_count = {} |
||||
|
|
||||
|
for lead in crm_model: |
||||
|
stage_name = lead.stage_id.name |
||||
|
if stage_name in stage_lead_count: |
||||
|
stage_lead_count[stage_name] += 1 |
||||
|
else: |
||||
|
stage_lead_count[stage_name] = 1 |
||||
|
|
||||
|
# Convert the dictionary into lists for stages and their counts |
||||
|
crm_stages = list(stage_lead_count.keys()) |
||||
|
lead_count = list(stage_lead_count.values()) |
||||
|
|
||||
|
# Return the data in the expected format |
||||
|
return [lead_count, crm_stages] |
||||
|
|
||||
|
@api.model |
||||
|
def get_lead_by_month(self): |
||||
|
"""pie chart""" |
||||
|
month_count = [] |
||||
|
month_value = [] |
||||
|
for rec in self.search([]): |
||||
|
month = rec.create_date.month |
||||
|
if month not in month_value: |
||||
|
month_value.append(month) |
||||
|
month_count.append(month) |
||||
|
month_val = [{'label': calendar.month_name[month], |
||||
|
'value': month_count.count(month)} for month in |
||||
|
month_value] |
||||
|
names = [record['label'] for record in month_val] |
||||
|
counts = [record['value'] for record in month_val] |
||||
|
month = [counts, names] |
||||
|
return month |
||||
|
|
||||
|
@api.model |
||||
|
def get_crm_activities(self, period): |
||||
|
"""Sales Activity Pie""" |
||||
|
start_date = get_period_start_date(period) |
||||
|
self._cr.execute(''' |
||||
|
SELECT mail_activity_type.name, COUNT(*) |
||||
|
FROM mail_activity |
||||
|
INNER JOIN mail_activity_type |
||||
|
ON mail_activity.activity_type_id = mail_activity_type.id |
||||
|
INNER JOIN crm_lead |
||||
|
ON mail_activity.res_id = crm_lead.id |
||||
|
AND mail_activity.res_model = 'crm.lead' |
||||
|
WHERE crm_lead.create_date >= %s |
||||
|
GROUP BY mail_activity_type.name |
||||
|
''', (start_date,)) |
||||
|
data = self._cr.dictfetchall() |
||||
|
names = [record['name']['en_US'] for record in data] |
||||
|
counts = [record['count'] for record in data] |
||||
|
return [counts, names] |
||||
|
|
||||
|
@api.model |
||||
|
def get_the_campaign_pie(self, period): |
||||
|
"""Leads Group By Campaign Pie""" |
||||
|
start_date = get_period_start_date(period) |
||||
|
self._cr.execute('''SELECT campaign_id, COUNT(*), |
||||
|
(SELECT name FROM utm_campaign |
||||
|
WHERE utm_campaign.id = crm_lead.campaign_id) |
||||
|
FROM crm_lead WHERE create_date >= %s AND campaign_id IS NOT NULL GROUP BY |
||||
|
campaign_id''', (start_date,)) |
||||
|
data = self._cr.dictfetchall() |
||||
|
names = [record.get('name') for record in data] |
||||
|
counts = [record.get('count') for record in data] |
||||
|
final = [counts, names] |
||||
|
return final |
||||
|
|
||||
|
@api.model |
||||
|
def get_the_source_pie(self, period): |
||||
|
"""Leads Group By Source Pie""" |
||||
|
start_date = get_period_start_date(period) |
||||
|
self._cr.execute('''SELECT source_id, COUNT(*), |
||||
|
(SELECT name FROM utm_source |
||||
|
WHERE utm_source.id = crm_lead.source_id) |
||||
|
FROM crm_lead WHERE create_date >= %s AND source_id IS NOT NULL GROUP BY |
||||
|
source_id''', (start_date,)) |
||||
|
data = self._cr.dictfetchall() |
||||
|
names = [record.get('name') for record in data] |
||||
|
counts = [record.get('count') for record in data] |
||||
|
final = [counts, names] |
||||
|
return final |
||||
|
|
||||
|
@api.model |
||||
|
def get_the_medium_pie(self, period): |
||||
|
"""Leads Group By Medium Pie""" |
||||
|
start_date = get_period_start_date(period) |
||||
|
self._cr.execute('''SELECT medium_id, COUNT(*), |
||||
|
(SELECT name FROM utm_medium |
||||
|
WHERE utm_medium.id = crm_lead.medium_id) |
||||
|
FROM crm_lead WHERE create_date >= %s AND medium_id IS NOT NULL GROUP BY medium_id''', |
||||
|
(start_date,)) |
||||
|
data = self._cr.dictfetchall() |
||||
|
names = [record.get('name') for record in data] |
||||
|
counts = [record.get('count') for record in data] |
||||
|
final = [counts, names] |
||||
|
return final |
||||
|
|
||||
|
@api.model |
||||
|
def get_total_lost_crm(self, period): |
||||
|
"""Lost Opportunity or Lead Graph""" |
||||
|
month_dict = {} |
||||
|
|
||||
|
# Format the start date to be used in the SQL query |
||||
|
start_date = get_period_start_date(period) |
||||
|
|
||||
|
if period == 'year': |
||||
|
num_months = 12 |
||||
|
elif period == 'quarter': |
||||
|
num_months = 3 |
||||
|
else: |
||||
|
num_months = 1 |
||||
|
|
||||
|
# Initialize the dictionary with month names and counts |
||||
|
for i in range(num_months): |
||||
|
current_month = start_date + relativedelta(months=i) |
||||
|
month_name = current_month.strftime('%B') |
||||
|
month_dict[month_name] = 0 |
||||
|
|
||||
|
# Execute the SQL query to count lost opportunities |
||||
|
self._cr.execute('''SELECT TO_CHAR(create_date, 'Month') AS month, |
||||
|
COUNT(id) |
||||
|
FROM crm_lead |
||||
|
WHERE probability = 0 |
||||
|
AND active = FALSE |
||||
|
AND create_date >= %s |
||||
|
GROUP BY TO_CHAR(create_date, 'Month') |
||||
|
ORDER BY TO_CHAR(create_date, 'Month')''', |
||||
|
(start_date,)) |
||||
|
|
||||
|
data = self._cr.dictfetchall() |
||||
|
|
||||
|
# Update month_dict with the results from the query |
||||
|
for rec in data: |
||||
|
month_name = rec[ |
||||
|
'month'].strip() # Strip the month name to remove extra spaces |
||||
|
if month_name in month_dict: |
||||
|
month_dict[month_name] = rec['count'] |
||||
|
|
||||
|
result = { |
||||
|
'month': list(month_dict.keys()), |
||||
|
'count': list(month_dict.values()) |
||||
|
} |
||||
|
|
||||
|
return result |
||||
|
|
||||
|
@api.model |
||||
|
def get_upcoming_events(self): |
||||
|
"""Upcoming Activities Table""" |
||||
|
today = fields.date.today() |
||||
|
session_user_id = self.env.uid |
||||
|
self._cr.execute('''select mail_activity.activity_type_id, |
||||
|
mail_activity.date_deadline, mail_activity.summary, |
||||
|
mail_activity.res_name,(SELECT mail_activity_type.name |
||||
|
FROM mail_activity_type WHERE mail_activity_type.id = |
||||
|
mail_activity.activity_type_id), mail_activity.user_id FROM |
||||
|
mail_activity WHERE res_model = 'crm.lead' AND |
||||
|
mail_activity.date_deadline >= '%s' and user_id = %s GROUP BY |
||||
|
mail_activity.activity_type_id, mail_activity.date_deadline, |
||||
|
mail_activity.summary,mail_activity.res_name,mail_activity.user_id |
||||
|
order by mail_activity.date_deadline asc''' % ( |
||||
|
today, session_user_id)) |
||||
|
data = self._cr.fetchall() |
||||
|
events = [[record[0], record[1], record[2], record[3], |
||||
|
record[4] if record[4] else '', |
||||
|
self.env['res.users'].browse(record[5]).name if record[ |
||||
|
5] else '' |
||||
|
] for record in data] |
||||
|
return { |
||||
|
'event': events, |
||||
|
'cur_lang': self.env.context.get('lang') |
||||
|
} |
||||
|
|
||||
|
@api.model |
||||
|
def total_revenue_by_sales(self, period): |
||||
|
"""Total expected revenue and count Pie""" |
||||
|
session_user_id = self.env.uid |
||||
|
start_date = get_period_start_date(period) |
||||
|
# SQL query template |
||||
|
query_template = """ |
||||
|
SELECT sum(expected_revenue) as revenue |
||||
|
FROM crm_lead |
||||
|
WHERE user_id = %s |
||||
|
AND type = 'opportunity' |
||||
|
AND active = %s |
||||
|
{conditions} |
||||
|
""" |
||||
|
|
||||
|
# Query conditions for different cases |
||||
|
conditions = [ |
||||
|
"", # Active opportunities |
||||
|
"AND stage_id = '4'", # Won opportunities |
||||
|
"AND probability = '0'", # Lost opportunities |
||||
|
] |
||||
|
|
||||
|
# Active status for each condition |
||||
|
active_status = ['true', 'false', 'false'] |
||||
|
|
||||
|
# Fetch total revenue for each condition |
||||
|
revenues = [] |
||||
|
for cond, active in zip(conditions, active_status): |
||||
|
self._cr.execute(query_template.format(conditions=cond), |
||||
|
(session_user_id, active)) |
||||
|
revenue = self._cr.fetchone()[0] or 0 |
||||
|
revenues.append(revenue) |
||||
|
|
||||
|
# Calculate expected revenue without won |
||||
|
exp_revenue_without_won = revenues[0] - revenues[1] |
||||
|
|
||||
|
# Prepare the data for the pie chart |
||||
|
revenue_pie_count = [exp_revenue_without_won, revenues[1], revenues[2]] |
||||
|
revenue_pie_title = ['Expected without Won', 'Won', 'Lost'] |
||||
|
|
||||
|
return [revenue_pie_count, revenue_pie_title] |
||||
|
|
||||
|
|
||||
|
@api.model |
||||
|
def get_top_sp_revenue(self,period): |
||||
|
"""Top 10 Salesperson revenue Table""" |
||||
|
user = self.env.user |
||||
|
start_date = get_period_start_date(period) |
||||
|
self._cr.execute('''SELECT user_id, id, expected_revenue, name, company_id |
||||
|
FROM crm_lead |
||||
|
WHERE create_date >= '%s' AND expected_revenue IS NOT NULL AND user_id = %s |
||||
|
GROUP BY user_id, id |
||||
|
ORDER BY expected_revenue DESC |
||||
|
LIMIT 10''' % (start_date,user.id,)) |
||||
|
data1 = self._cr.fetchall() |
||||
|
top_revenue = [ |
||||
|
[self.env['res.users'].browse(rec[0]).name, rec[1], rec[2], |
||||
|
rec[3], self.env['res.company'].browse(rec[4]).currency_id.symbol] |
||||
|
for rec in data1] |
||||
|
return {'top_revenue': top_revenue} |
||||
|
|
||||
|
@api.model |
||||
|
def get_top_country_revenue(self, period): |
||||
|
"""Top 10 Country Wise Revenue - Heat Map""" |
||||
|
company_id = self.env.company.id |
||||
|
self._cr.execute('''SELECT country_id, sum(expected_revenue) |
||||
|
FROM crm_lead |
||||
|
WHERE expected_revenue IS NOT NULL |
||||
|
AND country_id IS NOT NULL |
||||
|
GROUP BY country_id |
||||
|
ORDER BY sum(expected_revenue) DESC |
||||
|
LIMIT 10''') |
||||
|
data1 = self._cr.fetchall() |
||||
|
country_revenue = [[self.env['res.country'].browse(rec[0]).name, |
||||
|
rec[1], self.env['res.company'].browse( |
||||
|
company_id).currency_id.symbol] for rec in data1] |
||||
|
return {'country_revenue': country_revenue} |
||||
|
|
||||
|
@api.model |
||||
|
def get_top_country_count(self, period): |
||||
|
"""Top 10 Country Wise Count - Heat Map""" |
||||
|
self._cr.execute('''SELECT country_id, COUNT(*) |
||||
|
FROM crm_lead |
||||
|
WHERE country_id IS NOT NULL |
||||
|
GROUP BY country_id |
||||
|
ORDER BY COUNT(*) DESC |
||||
|
LIMIT 10''') |
||||
|
data1 = self._cr.fetchall() |
||||
|
country_count = [[self.env['res.country'].browse(rec[0]).name, rec[1]] |
||||
|
for rec in data1] |
||||
|
return {'country_count': country_count} |
||||
|
|
||||
|
@api.model |
||||
|
def get_recent_activities(self, kwargs): |
||||
|
"""Recent Activities Table""" |
||||
|
today = fields.Date.today() |
||||
|
recent_week = today - relativedelta(days=7) |
||||
|
current_user_id = self.env.user.id # Get the current logged-in user's ID |
||||
|
# Check if the current user is an administrator |
||||
|
is_admin = self.env.user.has_group('base.group_system') |
||||
|
# Build the SQL query with or without user filtering based on role |
||||
|
if is_admin: |
||||
|
self._cr.execute(''' |
||||
|
SELECT mail_activity.activity_type_id, |
||||
|
mail_activity.date_deadline, |
||||
|
mail_activity.summary, |
||||
|
mail_activity.res_name, |
||||
|
(SELECT mail_activity_type.name |
||||
|
FROM mail_activity_type |
||||
|
WHERE mail_activity_type.id = mail_activity.activity_type_id), |
||||
|
mail_activity.user_id |
||||
|
FROM mail_activity |
||||
|
WHERE res_model = 'crm.lead' |
||||
|
AND mail_activity.date_deadline BETWEEN %s AND %s |
||||
|
GROUP BY mail_activity.activity_type_id, |
||||
|
mail_activity.date_deadline, |
||||
|
mail_activity.summary, |
||||
|
mail_activity.res_name, |
||||
|
mail_activity.user_id |
||||
|
ORDER BY mail_activity.date_deadline DESC |
||||
|
''', (recent_week, today)) |
||||
|
else: |
||||
|
self._cr.execute(''' |
||||
|
SELECT mail_activity.activity_type_id, |
||||
|
mail_activity.date_deadline, |
||||
|
mail_activity.summary, |
||||
|
mail_activity.res_name, |
||||
|
(SELECT mail_activity_type.name |
||||
|
FROM mail_activity_type |
||||
|
WHERE mail_activity_type.id = mail_activity.activity_type_id), |
||||
|
mail_activity.user_id |
||||
|
FROM mail_activity |
||||
|
WHERE res_model = 'crm.lead' |
||||
|
AND mail_activity.date_deadline BETWEEN %s AND %s |
||||
|
AND mail_activity.user_id = %s |
||||
|
GROUP BY mail_activity.activity_type_id, |
||||
|
mail_activity.date_deadline, |
||||
|
mail_activity.summary, |
||||
|
mail_activity.res_name, |
||||
|
mail_activity.user_id |
||||
|
ORDER BY mail_activity.date_deadline DESC |
||||
|
''', (recent_week, today, current_user_id)) |
||||
|
|
||||
|
data = self._cr.fetchall() |
||||
|
activities = [ |
||||
|
[*record[:5], self.env['res.users'].browse(record[5]).name] for |
||||
|
record in data] |
||||
|
return {'activities': activities} |
@ -0,0 +1,34 @@ |
|||||
|
################################################################################ |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
from odoo import fields, models |
||||
|
|
||||
|
|
||||
|
class CRMSalesTeam(models.Model): |
||||
|
"""CRMSalesTeam model extends the base crm.team model to add a field, |
||||
|
crm_lead_state_id, which represents the default CRM Lead stage for |
||||
|
leads associated with this sales team. |
||||
|
""" |
||||
|
_inherit = 'crm.team' |
||||
|
|
||||
|
crm_lead_state_id = fields.Many2one("crm.stage", string="CRM Lead", |
||||
|
store=True, |
||||
|
help="CRM Lead stage for leads " |
||||
|
"associated with this sales team.") |
@ -0,0 +1,30 @@ |
|||||
|
################################################################################ |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
from odoo import fields, models |
||||
|
|
||||
|
|
||||
|
class ResUser(models.Model): |
||||
|
"""ResUser model extends the base res. Users model to add a field 'sales' |
||||
|
that represents the target for the salesperson.""" |
||||
|
_inherit = 'res.users' |
||||
|
|
||||
|
sales = fields.Float(string="Target", help="The target value for the " |
||||
|
"salesperson.") |
@ -0,0 +1,35 @@ |
|||||
|
################################################################################ |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
from odoo import models |
||||
|
|
||||
|
|
||||
|
class SalesOrder(models.Model): |
||||
|
"""Extends sale order for overriding action confirm function""" |
||||
|
_inherit = 'sale.order' |
||||
|
|
||||
|
def action_confirm(self): |
||||
|
"""Override the action_confirm method to change CRM Stage. |
||||
|
Returns: |
||||
|
dict: A dictionary containing the result of the original |
||||
|
action_confirm method.""" |
||||
|
res = super(SalesOrder, self).action_confirm() |
||||
|
self.opportunity_id.stage_id = self.team_id.crm_lead_state_id |
||||
|
return res |
@ -0,0 +1,63 @@ |
|||||
|
################################################################################ |
||||
|
# |
||||
|
# Cybrosys Technologies Pvt. Ltd. |
||||
|
# |
||||
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). |
||||
|
# Author: Raneesha (odoo@cybrosys.com) |
||||
|
# |
||||
|
# You can modify it under the terms of the GNU AFFERO |
||||
|
# GENERAL PUBLIC LICENSE (AGPL v3), Version 3. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE |
||||
|
# (AGPL v3) along with this program. |
||||
|
# If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
################################################################################ |
||||
|
from odoo import fields, models |
||||
|
|
||||
|
|
||||
|
class CampaignSmartButton(models.Model): |
||||
|
"""Extends the UTM Campaign model with a Smart Button to calculate and |
||||
|
display the Win Loss Ratio.""" |
||||
|
_inherit = 'utm.campaign' |
||||
|
|
||||
|
total_ratio = fields.Float(compute='_compute_ratio', |
||||
|
help="Total lead ratio") |
||||
|
|
||||
|
def get_ratio(self): |
||||
|
"""Open the Win Loss Ratio window upon clicking the Smart Button. |
||||
|
Returns: |
||||
|
dict: A dictionary specifying the action to be taken upon button |
||||
|
click.""" |
||||
|
self.ensure_one() |
||||
|
return { |
||||
|
'type': 'ir.actions.act_window', |
||||
|
'name': 'Win Loss Ratio', |
||||
|
'view_mode': 'kanban', |
||||
|
'res_model': 'crm.lead', |
||||
|
'domain': [['user_id', '=', self.env.uid], "|", |
||||
|
"&", ["active", "=", True], ["probability", '=', 100], |
||||
|
"&", ["active", "=", False], ["probability", '=', 0] |
||||
|
], |
||||
|
'context': "{'create': False,'records_draggable': False}" |
||||
|
} |
||||
|
|
||||
|
def _compute_ratio(self): |
||||
|
"""Compute the Win Loss Ratio based on CRM lead statistics.""" |
||||
|
total_won = self.env['crm.lead'].search_count( |
||||
|
[('active', '=', True), ('probability', '=', 100), |
||||
|
('user_id', '=', self.env.uid)]) |
||||
|
total_lose = self.env['crm.lead'].search_count( |
||||
|
[('active', '=', False), ('probability', '=', 0), |
||||
|
('user_id', '=', self.env.uid)]) |
||||
|
|
||||
|
if total_lose == 0: |
||||
|
ratio = 0 |
||||
|
else: |
||||
|
ratio = round(total_won / total_lose, 2) |
||||
|
self.total_ratio = ratio |
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: 767 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 760 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 697 KiB |
After Width: | Height: | Size: 1.1 MiB |
After Width: | Height: | Size: 171 KiB |
After Width: | Height: | Size: 198 KiB |
After Width: | Height: | Size: 173 KiB |
After Width: | Height: | Size: 138 KiB |
After Width: | Height: | Size: 151 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 179 KiB |
After Width: | Height: | Size: 142 KiB |
After Width: | Height: | Size: 213 KiB |
After Width: | Height: | Size: 880 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 8.4 KiB |
@ -0,0 +1,295 @@ |
|||||
|
/* Dashboard Main Section Styles */ |
||||
|
.dashboard_main_section { |
||||
|
padding-top: 2rem; |
||||
|
padding-bottom: 1rem; |
||||
|
background-color: #f8f9fa; |
||||
|
} |
||||
|
|
||||
|
.section-header { |
||||
|
font-size: 1.5rem; |
||||
|
font-weight: bold; |
||||
|
color: #343a40; |
||||
|
} |
||||
|
|
||||
|
/* Period Selection Styles */ |
||||
|
#period_selection { |
||||
|
margin-top: 1rem; |
||||
|
width: 100%; |
||||
|
height: 38px; |
||||
|
border: 1px solid #ced4da; |
||||
|
border-radius: 0.25rem; |
||||
|
background-color: #fff; |
||||
|
color: #495057; |
||||
|
font-size: 1rem; |
||||
|
line-height: 1.5; |
||||
|
padding: 0.375rem 1.75rem 0.375rem 0.75rem; |
||||
|
appearance: none; |
||||
|
-webkit-appearance: none; |
||||
|
-moz-appearance: none; |
||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E"); |
||||
|
background-repeat: no-repeat; |
||||
|
background-position: right 0.75rem center; |
||||
|
background-size: 16px 12px; |
||||
|
} |
||||
|
|
||||
|
#period_selection:focus { |
||||
|
outline: none; |
||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); |
||||
|
} |
||||
|
|
||||
|
/* Responsive Styles */ |
||||
|
@media (max-width: 767px) { |
||||
|
.section-header { |
||||
|
font-size: 1.25rem; |
||||
|
} |
||||
|
|
||||
|
#period_selection { |
||||
|
margin-top: 0.5rem; |
||||
|
height: 32px; |
||||
|
font-size: 0.875rem; |
||||
|
padding: 0.25rem 1.5rem 0.25rem 0.5rem; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* Dashboard Card Section Styles */ |
||||
|
.dashboard_card_section { |
||||
|
padding-top: 1rem; |
||||
|
padding-bottom: 2rem; |
||||
|
} |
||||
|
|
||||
|
/* Dashboard Card Styles */ |
||||
|
.dashboard-card { |
||||
|
position: relative; |
||||
|
margin-bottom: 1rem; |
||||
|
padding: 1rem; |
||||
|
border-radius: 0.5rem; |
||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
||||
|
background-color: #fff; |
||||
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card:hover { |
||||
|
transform: translateY(-4px); |
||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__icon-container { |
||||
|
width: 3rem; |
||||
|
height: 3rem; |
||||
|
font-size: 1.5rem; |
||||
|
color: #797979; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
border-radius: 50%; |
||||
|
background-color: #f0f0f0; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__details { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
flex: 1; |
||||
|
padding-left: 1rem; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__details h3, |
||||
|
.dashboard-card__details h4 { |
||||
|
margin-bottom: 0; |
||||
|
color: #333; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__details h3 { |
||||
|
font-size: 1.25rem; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__details h4 { |
||||
|
font-size: 1rem; |
||||
|
color: #666; |
||||
|
} |
||||
|
|
||||
|
/* Specific Card Styles */ |
||||
|
.card-shadow { |
||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.bg-mauve-light { |
||||
|
background-color: #f9f7ff; |
||||
|
} |
||||
|
|
||||
|
.text-mauve { |
||||
|
color: #7c4dff; |
||||
|
} |
||||
|
|
||||
|
/* Responsive Styles */ |
||||
|
@media (max-width: 767px) { |
||||
|
.dashboard-card { |
||||
|
padding: 0.75rem; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__icon-container { |
||||
|
width: 2.5rem; |
||||
|
height: 2.5rem; |
||||
|
font-size: 1.25rem; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__details h3 { |
||||
|
font-size: 1rem; |
||||
|
} |
||||
|
|
||||
|
.dashboard-card__details h4 { |
||||
|
font-size: 0.875rem; |
||||
|
} |
||||
|
} |
||||
|
/* Modern Minimalistic Kanban Card Style */ |
||||
|
/* Card Layout: Date as the Main Element */ |
||||
|
.upcoming_activities_div .chart-container { |
||||
|
padding: 20px; |
||||
|
background-color: #ffffff; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
|
||||
|
.upcoming_activities_div h3 { |
||||
|
margin-bottom: 20px; |
||||
|
font-weight: 600; |
||||
|
font-size: 18px; |
||||
|
color: #444; |
||||
|
} |
||||
|
|
||||
|
.recent_activity_div h3 { |
||||
|
margin-bottom: 20px; |
||||
|
font-weight: 600; |
||||
|
font-size: 18px; |
||||
|
color: #444; |
||||
|
} |
||||
|
.recent_activity_div .chart-container { |
||||
|
padding: 20px; |
||||
|
background-color: #ffffff; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
|
||||
|
.top_sp_revenue_div .chart-container { |
||||
|
padding: 20px; |
||||
|
background-color: #ffffff; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
.top_sp_revenue_div h3 { |
||||
|
margin-bottom: 20px; |
||||
|
font-weight: 600; |
||||
|
font-size: 18px; |
||||
|
color: #444; |
||||
|
} |
||||
|
.top_country_revenue_div .chart-container { |
||||
|
padding: 20px; |
||||
|
background-color: #ffffff; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
.top_country_revenue_div h3 { |
||||
|
margin-bottom: 20px; |
||||
|
font-weight: 600; |
||||
|
font-size: 18px; |
||||
|
color: #444; |
||||
|
} |
||||
|
.top_country_count_div .chart-container { |
||||
|
padding: 20px; |
||||
|
background-color: #ffffff; |
||||
|
border-radius: 12px; |
||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
|
} |
||||
|
.top_country_count_div h3 { |
||||
|
margin-bottom: 20px; |
||||
|
font-weight: 600; |
||||
|
font-size: 18px; |
||||
|
color: #444; |
||||
|
} |
||||
|
.crm_scroll_table { |
||||
|
overflow-y: auto; |
||||
|
max-height: 530px; |
||||
|
} |
||||
|
|
||||
|
.items-table { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
gap: 15px; |
||||
|
} |
||||
|
|
||||
|
.item-container { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.item-header { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
padding: 15px; |
||||
|
background-color: #f7f7f7; |
||||
|
border-radius: 10px; |
||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); |
||||
|
transition: box-shadow 0.3s ease; |
||||
|
} |
||||
|
|
||||
|
.item-header:hover { |
||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
||||
|
} |
||||
|
|
||||
|
.date-container { |
||||
|
flex-shrink: 0; |
||||
|
background-color: #4c8bf5; |
||||
|
color: #ffffff; |
||||
|
font-size: 16px; |
||||
|
font-weight: 700; |
||||
|
padding: 10px; |
||||
|
border-radius: 8px; |
||||
|
text-align: center; |
||||
|
margin-right: 20px; |
||||
|
min-width: 70px; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.date-container .day { |
||||
|
font-size: 24px; |
||||
|
font-weight: 800; |
||||
|
} |
||||
|
|
||||
|
.date-container .month { |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.item-content { |
||||
|
flex-grow: 1; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
|
||||
|
.item-content .item-title { |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
color: #333; |
||||
|
margin-bottom: 5px; |
||||
|
} |
||||
|
|
||||
|
.item-content .item-summary { |
||||
|
font-size: 14px; |
||||
|
color: #666; |
||||
|
margin-bottom: 3px; |
||||
|
} |
||||
|
|
||||
|
.item-content .item-extra { |
||||
|
font-size: 13px; |
||||
|
color: #888; |
||||
|
} |
||||
|
h2 { |
||||
|
padding: 8px; |
||||
|
} |
||||
|
canvas{ |
||||
|
padding-bottom: 9px; |
||||
|
} |
||||
|
#in_ex_body_hide{ |
||||
|
background: white; |
||||
|
} |
@ -0,0 +1,560 @@ |
|||||
|
/** @odoo-module **/ |
||||
|
import {registry} from "@web/core/registry"; |
||||
|
import {Component} from "@odoo/owl"; |
||||
|
import {onWillStart, onMounted, useState, useRef, useEffect} from "@odoo/owl"; |
||||
|
import {useService} from "@web/core/utils/hooks"; |
||||
|
|
||||
|
export class CrmDashboard extends Component { |
||||
|
setup() { |
||||
|
super.setup(...arguments); |
||||
|
this.orm = useService("orm"); |
||||
|
this.action = useService("action"); |
||||
|
this.Leadstage = useRef('leads_stage') |
||||
|
this.LeadByMonth = useRef('leads_by_month') |
||||
|
this.CrmActivities = useRef('crm_activities') |
||||
|
this.LeadByCampaign = useRef('leads_campaign') |
||||
|
this.LeadByMedium = useRef('leads_medium') |
||||
|
this.LeadBySource = useRef('leads_source') |
||||
|
this.LostLead = useRef('leads_lost') |
||||
|
this.TotalRevenue = useRef('total_revenue') |
||||
|
this.state = useState({ |
||||
|
period: 'month', |
||||
|
leads: null, |
||||
|
opportunities: null, |
||||
|
exp_revenue: null, |
||||
|
revenue: null, |
||||
|
win_ratio: null, |
||||
|
avg_close_time: null, |
||||
|
opportunity_ratio: null, |
||||
|
unassigned_leads: null, |
||||
|
charts: [], |
||||
|
upcoming_events: [], |
||||
|
current_lang: [], |
||||
|
top_sp_revenue: [], |
||||
|
country_count: [], |
||||
|
country_revenue: [], |
||||
|
recent_activities:[], |
||||
|
|
||||
|
}) |
||||
|
onWillStart(async () => { |
||||
|
await this.fetch_data(); |
||||
|
await this.UpcomingEvents(); |
||||
|
await this.TopSpRevenue(); |
||||
|
await this.TopCountryRevenue(); |
||||
|
await this.TopCountryCount(); |
||||
|
await this.RecentActivities(); |
||||
|
// Destroy existing chart if it exists
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (this.state.charts.length > 0) { |
||||
|
this.state.charts.forEach(chart => { |
||||
|
chart.destroy(); |
||||
|
}); |
||||
|
} |
||||
|
if (this.state.period) { |
||||
|
this.fetch_data(); |
||||
|
this.render_leads_by_stage(); |
||||
|
this.render_leads_by_month(); |
||||
|
this.render_crm_activities(); |
||||
|
this.render_lead_by_campaign(); |
||||
|
this.render_lead_by_medium(); |
||||
|
this.render_lead_by_source(); |
||||
|
this.render_lost_lead(); |
||||
|
this.render_total_revenue(); |
||||
|
} |
||||
|
}, () => [this.state.period]); |
||||
|
} |
||||
|
|
||||
|
async fetch_data() { |
||||
|
var self = this |
||||
|
var result = await this.orm.call('crm.lead', "get_data", [this.state.period]) |
||||
|
this.state.leads = result['leads'] |
||||
|
this.state.opportunities = result['opportunities'] |
||||
|
this.state.exp_revenue = result['exp_revenue'] |
||||
|
this.state.revenue = result['revenue'] |
||||
|
this.state.win_ratio = result['win_ratio'] |
||||
|
this.state.opportunity_ratio = result['opportunity_ratio'] |
||||
|
this.state.avg_close_time = result['avg_close_time'] |
||||
|
this.state.unassigned_leads = result['unassigned_leads'] |
||||
|
|
||||
|
} |
||||
|
|
||||
|
async UpcomingEvents() { |
||||
|
var result = await this.orm.call('crm.lead', "get_upcoming_events", []) |
||||
|
this.state.upcoming_events = result['event'] |
||||
|
this.state.current_lang = result['cur_lang'] |
||||
|
} |
||||
|
|
||||
|
async TopSpRevenue() { |
||||
|
var result = await this.orm.call('crm.lead', "get_top_sp_revenue", [this.state.period]) |
||||
|
this.state.top_sp_revenue = result['top_revenue'] |
||||
|
// this.state.current_lang = result['cur_lang']
|
||||
|
} |
||||
|
|
||||
|
async TopCountryCount() { |
||||
|
var result = await this.orm.call('crm.lead', "get_top_country_count", [this.state.period]) |
||||
|
this.state.country_count = result['country_count'] |
||||
|
|
||||
|
} |
||||
|
|
||||
|
async TopCountryRevenue() { |
||||
|
var result = await this.orm.call('crm.lead', "get_top_country_revenue", [this.state.period]) |
||||
|
this.state.country_revenue = result['country_revenue'] |
||||
|
} |
||||
|
async RecentActivities() { |
||||
|
var result = await this.orm.call('crm.lead', "get_recent_activities", [this.state.period]) |
||||
|
this.state.recent_activities = result['activities'] |
||||
|
} |
||||
|
|
||||
|
|
||||
|
SetPeriods() { |
||||
|
var today = new Date(); |
||||
|
var start_date; |
||||
|
|
||||
|
if (this.state.period == 'month') { |
||||
|
start_date = new Date(today.getFullYear(), today.getMonth(), 1); // Start of the month
|
||||
|
} else if (this.state.period == 'year') { |
||||
|
start_date = new Date(today.getFullYear(), 0, 1); // Start of the year
|
||||
|
} else if (this.state.period == 'quarter') { |
||||
|
var startMonth = Math.floor(today.getMonth() / 3) * 3; // Start month of the quarter
|
||||
|
start_date = new Date(today.getFullYear(), startMonth, 1); // Start of the quarter
|
||||
|
} else if (this.state.period == 'week') { |
||||
|
var dayOfWeek = today.getDay(); |
||||
|
var diff = today.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // Adjust when day is Sunday
|
||||
|
start_date = new Date(today.setDate(diff)); // Start of the week (Monday)
|
||||
|
} |
||||
|
|
||||
|
return start_date.getFullYear() + '-' + (start_date.getMonth() + 1).toString().padStart(2, '0') + '-' + start_date.getDate().toString().padStart(2, '0'); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
OnChangePeriods() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
onClickLeads() { |
||||
|
var date = this.SetPeriods() |
||||
|
this.action.doAction({ |
||||
|
type: "ir.actions.act_window", |
||||
|
name: "Leads", |
||||
|
res_model: 'crm.lead', |
||||
|
views: [[false, "kanban"], [false, "form"]], |
||||
|
target: "current", |
||||
|
domain: [['type', '=', 'lead'], ['create_date', '>=', date]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onClickOpportunities() { |
||||
|
var date = this.SetPeriods() |
||||
|
this.action.doAction({ |
||||
|
type: "ir.actions.act_window", |
||||
|
name: "Opportunities", |
||||
|
res_model: 'crm.lead', |
||||
|
views: [[false, "kanban"], [false, "form"]], |
||||
|
target: "current", |
||||
|
domain: [['type', '=', 'opportunity'], ['create_date', '>=', date]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onClickExpRevenue() { |
||||
|
var date = this.SetPeriods() |
||||
|
this.action.doAction({ |
||||
|
type: "ir.actions.act_window", |
||||
|
name: "Expense Revenue", |
||||
|
res_model: 'crm.lead', |
||||
|
views: [[false, "list"], [false, "form"]], |
||||
|
target: "current", |
||||
|
domain: [['type', '=', 'opportunity'], ['active', '=', true], ['create_date', '>=', date]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onClickRevenue() { |
||||
|
var date = this.SetPeriods() |
||||
|
this.action.doAction({ |
||||
|
type: "ir.actions.act_window", |
||||
|
name: "Revenue", |
||||
|
res_model: 'crm.lead', |
||||
|
views: [[false, "list"], [false, "form"]], |
||||
|
target: "current", |
||||
|
domain: [['type', '=', 'opportunity'], ['active', '=', true], ['stage_id', '=', 4], ['create_date', '>=', date]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
onClickUnAssignedLeads() { |
||||
|
var date = this.SetPeriods() |
||||
|
this.action.doAction({ |
||||
|
type: "ir.actions.act_window", |
||||
|
name: "Unassigned Leads", |
||||
|
res_model: 'crm.lead', |
||||
|
views: [[false, "list"], [false, "form"]], |
||||
|
target: "current", |
||||
|
domain: [['user_id', '=', false], ['type', '=', 'lead'], ['create_date', '>=', date]] |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
async render_leads_by_stage() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.Leadstage.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "get_lead_stage_data", [this.state.period]); |
||||
|
const data = { |
||||
|
labels: arrays[1], |
||||
|
datasets: [{ |
||||
|
label: 'Leads', |
||||
|
data: arrays[0], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'polarArea', |
||||
|
data: data, |
||||
|
|
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
|
||||
|
async render_leads_by_month() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.LeadByMonth.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "get_lead_by_month", []); |
||||
|
const data = { |
||||
|
labels: arrays[1], |
||||
|
datasets: [{ |
||||
|
label: 'Leads', |
||||
|
data: arrays[0], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'doughnut', |
||||
|
data: data, |
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
|
||||
|
async render_crm_activities() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.CrmActivities.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "get_crm_activities", [this.state.period]); |
||||
|
const data = { |
||||
|
labels: arrays[1], |
||||
|
datasets: [{ |
||||
|
label: 'Activity', |
||||
|
data: arrays[0], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'pie', |
||||
|
data: data, |
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
|
||||
|
async render_lead_by_campaign() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.LeadByCampaign.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "get_the_campaign_pie", [this.state.period]); |
||||
|
const data = { |
||||
|
labels: arrays[1], |
||||
|
datasets: [{ |
||||
|
label: 'Activity', |
||||
|
data: arrays[0], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'pie', |
||||
|
data: data, |
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
|
||||
|
async render_lead_by_medium() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.LeadByMedium.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "get_the_medium_pie", [this.state.period]); |
||||
|
const data = { |
||||
|
labels: arrays[1], |
||||
|
datasets: [{ |
||||
|
label: 'Activity', |
||||
|
data: arrays[0], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'pie', |
||||
|
data: data, |
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
|
||||
|
async render_lead_by_source() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.LeadBySource.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "get_the_source_pie", [this.state.period]); |
||||
|
const data = { |
||||
|
labels: arrays[1], |
||||
|
datasets: [{ |
||||
|
label: 'Activity', |
||||
|
data: arrays[0], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'pie', |
||||
|
data: data, |
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
|
||||
|
async render_lost_lead() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.LostLead.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "get_total_lost_crm", [this.state.period]); |
||||
|
const data = { |
||||
|
labels: arrays['month'], |
||||
|
datasets: [{ |
||||
|
label: 'Activity', |
||||
|
data: arrays['count'], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'bar', |
||||
|
data: data, |
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
|
||||
|
async render_total_revenue() { |
||||
|
|
||||
|
var self = this; |
||||
|
var ctx = this.TotalRevenue.el; |
||||
|
const arrays = await this.orm.call('crm.lead', "total_revenue_by_sales", [this.state.period]); |
||||
|
const data = { |
||||
|
labels: arrays[1], |
||||
|
datasets: [{ |
||||
|
label: 'Activity', |
||||
|
data: arrays[0], |
||||
|
backgroundColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
borderColor: [ |
||||
|
"#003f5c", |
||||
|
"#2f4b7c", |
||||
|
"#f95d6a", |
||||
|
"#665191", |
||||
|
"#d45087", |
||||
|
"#ff7c43", |
||||
|
"#ffa600", |
||||
|
"#a05195", |
||||
|
"#6d5c16" |
||||
|
], |
||||
|
}] |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
//create Chart class object
|
||||
|
var chart = new Chart(ctx, { |
||||
|
type: 'pie', |
||||
|
data: data, |
||||
|
// options: options
|
||||
|
}); |
||||
|
this.state.charts.push(chart) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
CrmDashboard.template = 'CrmDashboard' |
||||
|
registry.category("actions").add("crm_dashboard", CrmDashboard) |