You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
345 lines
14 KiB
345 lines
14 KiB
# -*- coding: utf-8 -*-
|
|
#############################################################################
|
|
#
|
|
# Cybrosys Technologies Pvt. Ltd.
|
|
#
|
|
# Copyright (C) 2025-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
|
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
|
|
#
|
|
# You can modify it under the terms of the GNU LESSER
|
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
|
|
#
|
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
|
|
# (LGPL v3) along with this program.
|
|
# If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
#############################################################################
|
|
from odoo import models, api, fields
|
|
from datetime import date, timedelta
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class SaleOrder(models.Model):
|
|
"""Extends sale.order model to fetch data to be displayed in the dashboard"""
|
|
_inherit = 'sale.order'
|
|
|
|
def _get_range(self, filter_key, custom_start=None, custom_end=None):
|
|
"""Manages the date range according to the filter selected"""
|
|
today = date.today()
|
|
|
|
if filter_key == 'this_week':
|
|
start = today - timedelta(days=today.weekday())
|
|
end = start + timedelta(days=6)
|
|
return start, min(end, today)
|
|
|
|
elif filter_key == 'this_month':
|
|
return today.replace(day=1), today
|
|
|
|
elif filter_key == 'this_year':
|
|
return today.replace(month=1, day=1), today
|
|
|
|
elif filter_key == 'custom' and custom_start and custom_end:
|
|
if custom_end < custom_start:
|
|
raise UserError("Please select a valid range")
|
|
return custom_start, custom_end
|
|
|
|
return None, None
|
|
|
|
def _build_global_domain(self, base_domain, filters, date_field="date_order"):
|
|
"""Build domain with global filter applied"""
|
|
global_filter = filters.get("global_filter", "this_week")
|
|
if global_filter == "select_period":
|
|
global_filter = "this_week"
|
|
|
|
if global_filter == "custom":
|
|
custom_range = filters.get("custom_range", {})
|
|
custom_start = custom_range.get("from")
|
|
custom_end = custom_range.get("to")
|
|
else:
|
|
custom_start = None
|
|
custom_end = None
|
|
|
|
from_date, to_date = self._get_range(global_filter, custom_start, custom_end)
|
|
|
|
domain = base_domain.copy()
|
|
if from_date:
|
|
domain.append((date_field, '>=', from_date))
|
|
if to_date:
|
|
domain.append((date_field, '<=', to_date))
|
|
return domain
|
|
|
|
@api.model
|
|
def get_tile_domain(self, base_domain, filters):
|
|
"""Get domain for tile clicks with global filter applied"""
|
|
filterss = self._build_global_domain(base_domain, filters)
|
|
return self._build_global_domain(base_domain, filters)
|
|
|
|
@api.model
|
|
def get_sales_dashboard_data(self, filters=None):
|
|
"""Fetches the datas to be displayed"""
|
|
filters = filters or {}
|
|
limit = int(filters.get("limit", 10))
|
|
|
|
def get_filter_key(specific_filter_key):
|
|
"""Build and return a domain combining base_domain with global date filters."""
|
|
global_filter = filters.get("global_filter", "this_week")
|
|
if global_filter == "custom":
|
|
return "custom"
|
|
elif global_filter == "select_period":
|
|
return filters.get(specific_filter_key, "this_week")
|
|
else:
|
|
return global_filter
|
|
|
|
def build_domain(base_domain, specific_filter_key, date_field="date_order"):
|
|
"""Attach date domain based on the filter key"""
|
|
filter_key = get_filter_key(specific_filter_key)
|
|
|
|
if filter_key == "custom":
|
|
custom_range = filters.get("custom_range", {})
|
|
custom_start = custom_range.get("from")
|
|
custom_end = custom_range.get("to")
|
|
else:
|
|
custom_start = None
|
|
custom_end = None
|
|
|
|
from_date, to_date = self._get_range(filter_key, custom_start, custom_end)
|
|
|
|
domain = base_domain.copy()
|
|
if from_date:
|
|
domain.append((date_field, '>=', from_date))
|
|
if to_date:
|
|
domain.append((date_field, '<=', to_date))
|
|
return domain
|
|
|
|
team_domain = build_domain([('state', 'in', ['sale', 'done'])], "team_filter")
|
|
sales_by_team = self.read_group(team_domain, ['amount_total'], ['team_id'], limit=limit, orderby='amount_total desc')
|
|
teams = [
|
|
{'id': rec['team_id'][0], 'name': rec['team_id'][1], 'amount': rec['amount_total']}
|
|
for rec in sales_by_team if rec['team_id']
|
|
]
|
|
|
|
person_domain = build_domain([('state', 'in', ['sale', 'done'])], "person_filter")
|
|
sales_by_person = self.read_group(person_domain, ['amount_total'], ['user_id'], limit=limit, orderby='amount_total desc')
|
|
persons = [
|
|
{'id': rec['user_id'][0], 'name': rec['user_id'][1], 'amount': rec['amount_total']}
|
|
for rec in sales_by_person if rec['user_id']
|
|
]
|
|
|
|
customer_domain = build_domain([('state', 'in', ['sale', 'done'])], "customer_filter")
|
|
customers_grouped = self.read_group(customer_domain, ['amount_total'], ['partner_id'],
|
|
limit=limit, orderby='amount_total desc')
|
|
customers = [
|
|
{'id': rec['partner_id'][0], 'name': rec['partner_id'][1], 'amount': rec['amount_total']}
|
|
for rec in customers_grouped if rec['partner_id']
|
|
]
|
|
|
|
product_domain = build_domain([('order_id.state', 'in', ['sale', 'done'])],
|
|
"product_filter", "order_id.date_order")
|
|
if filters.get("product_category_id"):
|
|
product_domain.append(('product_id.categ_id', '=', filters["product_category_id"]))
|
|
top_products_grouped = self.env['sale.order.line'].read_group(
|
|
product_domain, ['product_uom_qty'], ['product_id'],
|
|
limit=limit, orderby='product_uom_qty desc'
|
|
)
|
|
top_products = [
|
|
{'id': rec['product_id'][0], 'name': rec['product_id'][1], 'qty': rec['product_uom_qty']}
|
|
for rec in top_products_grouped if rec['product_id']
|
|
]
|
|
|
|
low_product_domain = build_domain([('order_id.state', 'in', ['sale', 'done'])],
|
|
"low_product_filter", "order_id.date_order")
|
|
if filters.get("low_product_category_id"):
|
|
low_product_domain.append(('product_id.categ_id', '=', filters["low_product_category_id"]))
|
|
low_products_grouped = self.env['sale.order.line'].read_group(
|
|
low_product_domain, ['product_uom_qty'], ['product_id'],
|
|
limit=limit, orderby='product_uom_qty asc'
|
|
)
|
|
low_products = [
|
|
{'id': rec['product_id'][0], 'name': rec['product_id'][1], 'qty': rec['product_uom_qty']}
|
|
for rec in low_products_grouped if rec['product_id']
|
|
]
|
|
|
|
order_domain = build_domain([], "order_filter")
|
|
order_status_grouped = self.read_group(order_domain, ['id'], ['state'])
|
|
ORDER_STATUS_LABELS = {
|
|
'draft': 'Quotation',
|
|
'sent': 'Quotation Sent',
|
|
'sale': 'Sales Order',
|
|
'done': 'Locked',
|
|
'cancel': 'Cancelled',
|
|
}
|
|
order_status = [
|
|
{'status': ORDER_STATUS_LABELS.get(rec['state'], rec['state'].capitalize()),
|
|
'count': rec['state_count']}
|
|
for rec in order_status_grouped
|
|
]
|
|
|
|
invoice_domain = build_domain([('move_type', '=', 'out_invoice')], "invoice_filter", "invoice_date")
|
|
invoice_status_grouped = self.env['account.move'].read_group(invoice_domain, ['id'], ['state'])
|
|
INVOICE_STATUS_LABELS = {
|
|
'draft': 'Draft',
|
|
'posted': 'Posted',
|
|
'cancel': 'Cancelled',
|
|
}
|
|
invoice_status = [
|
|
{'status': INVOICE_STATUS_LABELS.get(rec['state'], rec['state'].capitalize()),
|
|
'count': rec['state_count']}
|
|
for rec in invoice_status_grouped
|
|
]
|
|
|
|
overdue_customers_domain =build_domain([
|
|
('move_type', '=', 'out_invoice'),
|
|
('payment_state', '!=', 'paid'),
|
|
('invoice_date_due', '<', fields.Date.today())
|
|
],"overdue_filter","invoice_date")
|
|
overdue_customers_grouped = self.env['account.move'].read_group(
|
|
overdue_customers_domain, ['amount_total'], ['partner_id'], orderby='amount_total desc', limit=limit
|
|
)
|
|
overdue_customers =[
|
|
{'id': rec['partner_id'][0], 'name': rec['partner_id'][1], 'amount': rec['amount_total']}
|
|
for rec in overdue_customers_grouped if rec['partner_id']
|
|
]
|
|
|
|
categories = self.env['product.category'].search([])
|
|
product_categories = [{'id': c.id, 'name': c.display_name} for c in categories]
|
|
|
|
sale_orders_domain = build_domain([('state', 'in', ['sale', 'done'])], "order_tile_filter")
|
|
sale_orders = self.search_count(sale_orders_domain)
|
|
|
|
quotations_domain = build_domain([('state', 'in', ['draft', 'sent'])], "quotation_tile_filter")
|
|
quotations = self.search_count(quotations_domain)
|
|
|
|
orders_to_invoice_domain = build_domain([('invoice_status', '=', 'to invoice')], "to_invoice_tile_filter")
|
|
orders_to_invoice = self.search_count(orders_to_invoice_domain)
|
|
|
|
orders_fully_invoiced_domain = build_domain([('invoice_status', '=', 'invoiced')], "invoiced_tile_filter")
|
|
orders_fully_invoiced = self.search_count(orders_fully_invoiced_domain)
|
|
|
|
conversion_rate = round(
|
|
(sale_orders / (quotations + sale_orders) * 100) if (quotations + sale_orders) > 0 else 0)
|
|
|
|
SaleOrder = self.env['sale.order']
|
|
company_currency = self.env.company.currency_id
|
|
today = fields.Date.today()
|
|
month_start = today.replace(day=1)
|
|
year_start = today.replace(month=1, day=1)
|
|
|
|
def _get_total(start_date):
|
|
"""To find the total amount by grouping the orders based on the currency"""
|
|
groups = SaleOrder.read_group(
|
|
[('state', '=', 'sale'), ('date_order', '>=', start_date)],
|
|
['amount_total:sum'],
|
|
['currency_id']
|
|
)
|
|
total = 0.0
|
|
for g in groups:
|
|
if g['currency_id']:
|
|
currency = self.env['res.currency'].browse(g['currency_id'][0])
|
|
total += currency._convert(
|
|
g['amount_total'],
|
|
company_currency,
|
|
self.env.company,
|
|
today
|
|
)
|
|
return total
|
|
|
|
total_revenue_mtd = _get_total(month_start)
|
|
total_revenue_ytd = _get_total(year_start)
|
|
|
|
groups = SaleOrder.read_group(
|
|
[('state', '=', 'sale')],
|
|
['amount_total:sum', 'id:count'],
|
|
['currency_id']
|
|
)
|
|
|
|
total_revenue = 0.0
|
|
total_orders = 0
|
|
for g in groups:
|
|
if g['currency_id']:
|
|
currency = self.env['res.currency'].browse(g['currency_id'][0])
|
|
total_revenue += currency._convert(
|
|
g['amount_total'],
|
|
company_currency,
|
|
self.env.company,
|
|
today
|
|
)
|
|
total_orders += g.get('currency_id_count', 0)
|
|
|
|
avg_order_value = total_revenue / total_orders if total_orders else 0
|
|
|
|
sales_info = {
|
|
'sale_orders': sale_orders,
|
|
'quotation': quotations,
|
|
'orders_to_invoice': orders_to_invoice,
|
|
'orders_fully_invoiced': orders_fully_invoiced,
|
|
'conversion_rate': conversion_rate,
|
|
'total_revenue_mtd': company_currency.format(total_revenue_mtd),
|
|
'total_revenue_ytd': company_currency.format(total_revenue_ytd),
|
|
'avg_order_value': company_currency.format(avg_order_value),
|
|
}
|
|
|
|
nvrc_domain = build_domain([('state', 'in', ['sale', 'done'])], "nvrc_filter")
|
|
|
|
date_from, date_to = None, None
|
|
for d in nvrc_domain:
|
|
if d[0] == 'date_order' and d[1] == '>=':
|
|
date_from = d[2]
|
|
elif d[0] == 'date_order' and d[1] == '<=':
|
|
date_to = d[2]
|
|
|
|
if date_from and isinstance(date_from, str):
|
|
date_from = fields.Date.from_string(date_from)
|
|
if date_to and isinstance(date_to, str):
|
|
date_to = fields.Date.from_string(date_to)
|
|
|
|
current_customers = self.read_group(
|
|
nvrc_domain, ['partner_id'], ['partner_id']
|
|
)
|
|
customer_ids = [rec['partner_id'][0] for rec in current_customers if rec['partner_id']]
|
|
|
|
new_customers, returning_customers = [], []
|
|
if customer_ids:
|
|
first_orders = self.read_group(
|
|
[('partner_id', 'in', customer_ids), ('state', 'in', ['sale', 'done'])],
|
|
['partner_id', 'date_order:min'],
|
|
['partner_id']
|
|
)
|
|
first_order_map = {rec['partner_id'][0]: rec['date_order'] for rec in first_orders if rec['partner_id']}
|
|
|
|
partners = self.env['res.partner'].browse(customer_ids)
|
|
for partner in partners:
|
|
first_date = first_order_map.get(partner.id)
|
|
if first_date and date_from and first_date.date() >= date_from:
|
|
new_customers.append({'id': partner.id, 'name': partner.display_name})
|
|
else:
|
|
returning_customers.append({'id': partner.id, 'name': partner.display_name})
|
|
|
|
new_vs_returning = {
|
|
'summary': {
|
|
'labels': ["New Customers", "Returning Customers"],
|
|
'values': [len(new_customers), len(returning_customers)],
|
|
},
|
|
'details': {
|
|
'new': new_customers[:limit],
|
|
'returning': returning_customers[:limit],
|
|
}
|
|
}
|
|
|
|
return {
|
|
'sales_by_team': teams,
|
|
'sales_by_person': persons,
|
|
'top_customers': customers,
|
|
'top_products': top_products,
|
|
'lowest_products': low_products,
|
|
'overdue_customers': overdue_customers,
|
|
'order_status': order_status,
|
|
'invoice_status': invoice_status,
|
|
'product_categories': product_categories,
|
|
'sales_info': sales_info,
|
|
'new_vs_returning': new_vs_returning,
|
|
}
|