odoo18/demo_addons_modules/aui_custom_dashboards/controllers/dashboards.py

725 lines
25 KiB
Python

from odoo import http
from odoo.http import request
class AUIDashboardController(http.Controller):
@http.route("/aui/dashboard/overview", type="json", auth="user")
def get_overview_data(self):
env = request.env
# -------------------------------------------------
# BASIC COUNTS
# -------------------------------------------------
sales_count = env["sale.order"].search_count([])
purchase_count = env["purchase.order"].search_count([])
inventory_count = env["stock.quant"].search_count([])
work_orders_count = env["mrp.production"].search_count([])
landed_cost_count = env["stock.landed.cost"].search_count([
("state", "=", "done")
])
# -------------------------------------------------
# SALES & PURCHASE AMOUNTS
# -------------------------------------------------
sales_orders = env["sale.order"].search([
("state", "in", ["sale", "done"])
])
sales_amount = sum(sales_orders.mapped("amount_total"))
purchase_orders = env["purchase.order"].search([
("state", "in", ["purchase", "done"])
])
purchase_amount = sum(purchase_orders.mapped("amount_total"))
# -------------------------------------------------
# LANDED COST TOTAL
# -------------------------------------------------
landed_costs = env["stock.landed.cost"].search([
("state", "=", "done")
])
additional_exp_amount = sum(landed_costs.mapped("amount_total"))
# -------------------------------------------------
# LANDED COST LINES → DONUT DATA
# -------------------------------------------------
expense_map = {}
landed_cost_lines = env["stock.landed.cost.lines"].search([
("cost_id.state", "=", "done")
])
for line in landed_cost_lines:
product_name = line.product_id.display_name if line.product_id else "Other"
expense_map[product_name] = (
expense_map.get(product_name, 0.0)
+ (line.price_unit or 0.0)
)
additional_expenses = {
"labels": list(expense_map.keys()),
"series": list(expense_map.values()),
"total": sum(expense_map.values()),
"count": len(landed_cost_lines),
}
# -------------------------------------------------
# COST OF GOODS SOLD (RAW MATERIAL COST)
# -------------------------------------------------
# stock_moves = env["stock.move"].search([
# ("state", "=", "done"),
# ("sale_line_id", "!=", False),
# ("product_id.valuation", "!=", "manual_periodic"),
# ])
# stock.move.value is NEGATIVE for outgoing moves
# raw_material_cost = abs(sum(stock_moves.mapped("value")))
# -------------------------------------------------
# MANUFACTURING COST (OPTIONAL / CUSTOM)
# -------------------------------------------------
manufacturing_cost = 0.0
productions = env["mrp.production"].search([
("state", "=", "done")
])
# If you have custom cost field
if "extra_cost" in productions._fields:
manufacturing_cost = sum(productions.mapped("extra_cost"))
# -------------------------------------------------
# PROFIT CALCULATION
# -------------------------------------------------
total_cost = (
manufacturing_cost
+ additional_exp_amount
)
profit = sales_amount - total_cost
# -------------------------------------------------
# FINAL RESPONSE
# -------------------------------------------------
data = {
"sales": sales_count,
"purchase": purchase_count,
"inventory": inventory_count,
"workOrders": work_orders_count,
"landedCostCount": landed_cost_count,
"sales_amount": sales_amount,
"purchase_amount": purchase_amount,
"additional_exp_amount": additional_exp_amount,
"manufacturing_cost": manufacturing_cost,
"total_cost": total_cost,
"profit": profit,
"additional_expenses": additional_expenses,
}
return data
@http.route("/aui/dashboard/purchase_vendor_product_stacked", type="json", auth="user")
def get_purchase_vendor_product_stacked(self):
env = request.env
po_lines = env["purchase.order.line"].search([
("order_id.state", "in", ["purchase", "done"]),
])
# Structure:
# vendor_map = {
# "Vendor A": {"Product 1": 1000, "Product 2": 2000},
# "Vendor B": {"Product 1": 1500}
# }
vendor_map = {}
products_set = set()
for line in po_lines:
vendor = line.order_id.partner_id.name
product = line.product_id.display_name
amount = line.price_subtotal
products_set.add(product)
vendor_map.setdefault(vendor, {})
vendor_map[vendor][product] = (
vendor_map[vendor].get(product, 0.0) + amount
)
vendors = list(vendor_map.keys())
products = list(products_set)
# Build stacked series
series = []
for product in products:
series.append({
"name": product,
"data": [
vendor_map[vendor].get(product, 0.0)
for vendor in vendors
],
})
return {
"categories": vendors,
"series": series,
}
@http.route("/aui/dashboard/work_order_status", type="json", auth="user")
def get_work_order_status(self):
env = request.env
data = env["work.order"].read_group(
[],
["id:count"],
["state"]
)
labels = []
series = []
for row in data:
labels.append(dict(
env["work.order"]._fields["state"].selection
).get(row["state"]))
series.append(row["id"])
return {
"labels": labels,
"series": series,
}
@http.route("/aui/dashboard/finished_goods_output", type="json", auth="user")
def get_finished_goods_output(self):
env = request.env
data = env["work.order.inward"].read_group(
[("state", "=", "confirmed")],
["quantity:sum"],
["product_id"]
)
categories = []
series = [{
"name": "Produced Quantity",
"data": []
}]
for row in data:
categories.append(row["product_id"][1])
series[0]["data"].append(row["quantity"])
print({"categories": categories, "series": series})
return {
"categories": categories,
"series": series,
}
@http.route("/aui/dashboard/raw_material_consumption", type="json", auth="user")
def get_raw_material_consumption(self):
env = request.env
data = env["work.order.inward.raw.line"].read_group(
[],
["quantity:sum"],
["product_id"]
)
labels = []
series = []
for row in data:
labels.append(row["product_id"][1])
series.append(row["quantity"])
return {
"labels": labels,
"series": series,
}
@http.route("/aui/dashboard/work_order_material_flow", type="json", auth="user")
def get_work_order_material_flow(self):
env = request.env
# Limit to latest 6 work orders (best for demo)
work_orders = env["work.order"].search(
[("state", "in", ["partial", "done"])],
order="create_date desc",
limit=6
)
categories = []
sent_qty = []
used_qty = []
scrap_qty = []
finished_qty = []
for wo in work_orders:
categories.append(wo.name)
# Raw material SENT (KG)
sent = sum(wo.line_ids.mapped("quantity"))
# Raw material USED (KG)
used = sum(
env["work.order.inward.raw.line"]
.search([("inward_id.work_order_ids", "in", wo.id)])
.mapped("quantity")
)
# Scrap / loss (KG)
scrap = sum(
env["work.order.inward"]
.search([("work_order_ids", "in", wo.id)])
.mapped("difference")
)
# Finished goods (Units)
finished = sum(
env["work.order.inward"]
.search([("work_order_ids", "in", wo.id)])
.mapped("quantity")
)
sent_qty.append(sent)
used_qty.append(used)
scrap_qty.append(abs(scrap))
finished_qty.append(finished)
print({
"categories": categories,
"series": [
{
"name": "Sent (KG)",
"type": "bar",
"data": sent_qty,
},
{
"name": "Used (KG)",
"type": "bar",
"data": used_qty,
},
{
"name": "Scrap (KG)",
"type": "bar",
"data": scrap_qty,
},
{
"name": "Finished Units",
"type": "line",
"data": finished_qty,
},
],
})
return {
"categories": categories,
"series": [
{
"name": "Sent (KG)",
"type": "bar",
"data": sent_qty,
},
{
"name": "Used (KG)",
"type": "bar",
"data": used_qty,
},
{
"name": "Scrap (KG)",
"type": "bar",
"data": scrap_qty,
},
{
"name": "Finished Units",
"type": "line",
"data": finished_qty,
},
],
}
# Add this to your dashboard controller
@http.route('/aui/dashboard/work_order_analysis', type='json', auth='user')
def get_work_order_analysis(self):
# Get work orders with inward data
work_orders = request.env['work.order'].search([
('state', 'not in', ['draft', 'confirmed'])
], order='create_date desc', limit=20)
# Group by subcontractor
subcontractor_data = {}
for wo in work_orders:
subcontractor = wo.partner_id.name or 'Unknown'
if subcontractor not in subcontractor_data:
subcontractor_data[subcontractor] = {
'work_orders': [],
'total_raw_used': 0,
'total_fg_weight': 0,
'total_fg_units': 0,
'total_scrap': 0
}
# Get inward records for this work order
inward_ids = request.env['work.order.inward'].search([
('work_order_ids', 'in', [wo.id]),
('state', 'not in', ['draft'])
])
# Calculate totals
total_raw_sent = sum(wo.line_ids.mapped('quantity'))
total_raw_used = 0
total_fg_weight = 0
total_fg_units = 0
total_scrap = 0
inward_details = []
for inward in inward_ids:
# Raw material used
inward_raw_used = sum(inward.raw_line_ids.mapped('quantity'))
total_raw_used += inward_raw_used
# Finished goods
if inward.fg_weight:
total_fg_weight += inward.fg_weight
total_fg_units += inward.quantity
# Scrap
total_scrap += inward.difference or 0
# Store inward details for tooltip
inward_details.append({
'inward_name': inward.in_picking_id.display_name or f"Inward-{inward.id}",
'raw_material_used': inward_raw_used,
'fg_weight': inward.fg_weight or 0,
'fg_units': inward.quantity,
'scrap': inward.difference or 0,
'product_name': inward.product_id.display_name if inward.product_id else 'N/A'
})
wo_data = {
'work_order': wo.name,
'raw_material_sent': total_raw_sent,
'raw_material_used': total_raw_used,
'finished_goods_weight': total_fg_weight,
'finished_goods_units': total_fg_units,
'scrap': total_scrap,
'yield_percentage': (total_fg_weight / total_raw_used * 100) if total_raw_used > 0 else 0,
'status': wo.state,
'subcontractor': subcontractor,
'inward_details': inward_details
}
subcontractor_data[subcontractor]['work_orders'].append(wo_data)
# Update subcontractor totals
subcontractor_data[subcontractor]['total_raw_used'] += total_raw_used
subcontractor_data[subcontractor]['total_fg_weight'] += total_fg_weight
subcontractor_data[subcontractor]['total_fg_units'] += total_fg_units
subcontractor_data[subcontractor]['total_scrap'] += total_scrap
# Prepare series data for chart
series = [
{'name': 'Raw Material Used (kg)', 'data': []},
{'name': 'Finished Goods Weight (kg)', 'data': []},
{'name': 'Scrap/Difference (kg)', 'data': []}
]
categories = []
detailed_data = []
# Flatten data for chart - Group by subcontractor with work orders
for subcontractor, data in subcontractor_data.items():
for wo_data in data['work_orders']:
# Create category name: "Vendor - WO001"
category_name = f"{subcontractor} - {wo_data['work_order']}"
categories.append(category_name)
# Add data to series
series[0]['data'].append(wo_data['raw_material_used'])
series[1]['data'].append(wo_data['finished_goods_weight'])
series[2]['data'].append(wo_data['scrap'])
# Store detailed data for tooltips
detailed_data.append({
'category': category_name,
'work_order': wo_data['work_order'],
'subcontractor': subcontractor,
'raw_material_used': wo_data['raw_material_used'],
'finished_goods_weight': wo_data['finished_goods_weight'],
'finished_goods_units': wo_data['finished_goods_units'],
'scrap': wo_data['scrap'],
'yield_percentage': wo_data['yield_percentage'],
'status': wo_data['status'],
'inward_details': wo_data['inward_details']
})
return {
'series': series,
'categories': categories,
'detailed_data': detailed_data,
'subcontractor_summary': [
{
'name': k,
'total_raw_used': v['total_raw_used'],
'total_fg_weight': v['total_fg_weight'],
'total_fg_units': v['total_fg_units'],
'total_scrap': v['total_scrap'],
'work_order_count': len(v['work_orders'])
}
for k, v in subcontractor_data.items()
]
}
@http.route('/aui/dashboard/inventory_flow', type='json', auth='user')
def get_inventory_flow_data(self, category_ids=None, location_ids=None):
env = request.env
# Build domain for stock moves
domain = [('state', '=', 'done'), ('product_qty', '>', 0)]
if category_ids:
# Convert string IDs to integers if they come as strings
if isinstance(category_ids, str):
category_ids = [int(cat_id) for cat_id in category_ids.split(',')]
elif isinstance(category_ids, list):
category_ids = [int(cat_id) for cat_id in category_ids]
domain.append(('product_id.categ_id', 'in', category_ids))
if location_ids:
# Convert string IDs to integers
if isinstance(location_ids, str):
location_ids = [int(loc_id) for loc_id in location_ids.split(',')]
elif isinstance(location_ids, list):
location_ids = [int(loc_id) for loc_id in location_ids]
domain.append('|')
domain.append(('location_id', 'in', location_ids))
domain.append(('location_dest_id', 'in', location_ids))
# Get recent stock moves (limit to 50 for performance)
moves = env['stock.move'].search(domain, limit=50, order='date desc')
# Create nodes (unique locations)
nodes_set = set()
links = []
for move in moves:
if move.product_uom_qty <= 0:
continue
source = move.location_id.complete_name or 'Unknown Source'
target = move.location_dest_id.complete_name or 'Unknown Destination'
# Add to nodes set
nodes_set.add(source)
nodes_set.add(target)
# Create link
links.append({
'source': source,
'target': target,
'value': float(move.product_uom_qty),
'product': move.product_id.display_name if move.product_id else 'Unknown Product',
'move_name': move.reference or move.name or 'Move',
'date': move.date.strftime('%Y-%m-%d') if move.date else '',
'uom': move.product_uom.name if move.product_uom else 'units'
})
# Convert nodes set to list
nodes = [{'name': node} for node in nodes_set]
# Get available categories for filter
categories = env['product.category'].search_read([], ['id', 'name', 'complete_name'])
# Get available locations for filter
locations = env['stock.location'].search_read([
('usage', 'in', ['internal', 'transit']),
('company_id', '=', env.company.id)
], ['id', 'complete_name'])
return {
'nodes': nodes,
'links': links,
'filters': {
'categories': categories,
'locations': locations
},
'total_moves': len(links),
'total_quantity': sum(link['value'] for link in links) if links else 0
}
@http.route('/aui/dashboard/inventory_heatmap', type='json', auth='user')
def get_inventory_heatmap_data(self, category_ids=None, location_ids=None):
env = request.env
domain = [
('location_id.usage', '=', 'internal'),
('quantity', '>', 0),
('company_id', '=', env.company.id),
]
if location_ids:
location_ids = [int(x) for x in location_ids.split(',')] if isinstance(location_ids, str) else location_ids
domain.append(('location_id', 'in', location_ids))
if category_ids:
category_ids = [int(x) for x in category_ids.split(',')] if isinstance(category_ids, str) else category_ids
domain.append(('product_id.categ_id', 'in', category_ids))
quants = env['stock.quant'].search(domain)
# location → category → data
matrix = {}
all_locations = set()
all_categories = set()
for q in quants:
location = q.location_id.complete_name
category = q.product_id.categ_id.name or 'Uncategorized'
location = location.replace('WH/Stock', 'Main Warehouse')
location = location.replace('WO_', 'Work Order ')
location = location.replace('SO_', 'Sales Order ')
all_locations.add(location)
all_categories.add(category)
matrix.setdefault(location, {})
matrix[location].setdefault(category, {
'quantity': 0.0,
'products': set(),
})
matrix[location][category]['quantity'] += q.quantity
matrix[location][category]['products'].add(q.product_id.id)
# Find max qty for normalization
max_qty = max(
(cell['quantity'] for loc in matrix.values() for cell in loc.values()),
default=1
)
heatmap_data = []
for location in all_locations:
for category in all_categories:
cell = matrix.get(location, {}).get(category, {'quantity': 0, 'products': set()})
qty = cell['quantity']
heatmap_data.append({
'location': location,
'category': category,
'value': qty,
'product_count': len(cell['products']),
'intensity': round((qty / max_qty) * 100, 2),
})
return {
'locations': list(all_locations) if all_locations else [],
'categories': list(all_categories) if all_categories else [],
'data': heatmap_data if heatmap_data else [],
'total_quantity': sum(d['value'] for d in heatmap_data) if heatmap_data else 0,
}
# @http.route('/aui/dashboard/inventory_heatmap', type='json', auth='user')
# def get_inventory_heatmap_data(self, category_ids=None, location_ids=None):
# env = request.env
#
# domain = [
# ('location_id.usage', '=', 'internal'),
# ('quantity', '>', 0),
# ('company_id', '=', env.company.id)
# ]
#
# if location_ids:
# if isinstance(location_ids, str):
# location_ids = [int(loc_id) for loc_id in location_ids.split(',')]
# elif isinstance(location_ids, list):
# location_ids = [int(loc_id) for loc_id in location_ids]
# domain.append(('location_id', 'in', location_ids))
#
# if category_ids:
# if isinstance(category_ids, str):
# category_ids = [int(cat_id) for cat_id in category_ids.split(',')]
# elif isinstance(category_ids, list):
# category_ids = [int(cat_id) for cat_id in category_ids]
# domain.append(('product_id.categ_id', 'in', category_ids))
#
# # Get all stock quants
# quants = env['stock.quant'].search(domain)
#
# print(f"Found {len(quants)} stock quants")
#
# # Aggregate manually for better control
# aggregated_data = {}
#
# for quant in quants:
# if not quant.product_id or not quant.location_id:
# continue
#
# location_name = quant.location_id.complete_name
# category_name = quant.product_id.categ_id.name if quant.product_id.categ_id else 'Uncategorized'
#
# # Use full location path but clean it up
# location_name = location_name.replace('WH/Stock', 'Main Warehouse')
# location_name = location_name.replace('WO_', 'Work Order ')
# location_name = location_name.replace('SO_', 'Sales Order ')
#
# # Create a unique key for location-category combination
# key = f"{location_name}|{category_name}"
#
# if key in aggregated_data:
# aggregated_data[key]['value'] += quant.quantity
# aggregated_data[key]['product_count'] += 1
# else:
# aggregated_data[key] = {
# 'location': location_name,
# 'category': category_name,
# 'value': quant.quantity,
# 'product_count': 1
# }
#
# # Convert to list
# heatmap_data = list(aggregated_data.values())
#
# # Get unique locations and categories
# unique_locations = sorted(set(item['location'] for item in heatmap_data))
# unique_categories = sorted(set(item['category'] for item in heatmap_data))
#
# # Get filter options
# category_list = env['product.category'].search_read([], ['id', 'name'])
# location_list = env['stock.location'].search_read([
# ('usage', '=', 'internal'),
# ('company_id', '=', env.company.id)
# ], ['id', 'complete_name'])
# print({
# 'locations': unique_locations,
# 'categories': unique_categories,
# 'data': heatmap_data,
# 'total_quantity': sum(item['value'] for item in heatmap_data),
# 'filters': {
# 'categories': category_list,
# 'locations': location_list
# }
# })
# result = {
# 'locations': unique_locations,
# 'categories': unique_categories,
# 'data': heatmap_data,
# 'total_quantity': sum(item['value'] for item in heatmap_data),
# 'filters': {
# 'categories': category_list,
# 'locations': location_list
# }
# }
#
# print(f"Returning {len(heatmap_data)} data points")
# print(f"Unique locations: {unique_locations}")
# print(f"Unique categories: {unique_categories}")
#
# return result