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