from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError from collections import defaultdict from functools import reduce import pytz from datetime import datetime, time class SamashtiDashboard(models.AbstractModel): _name = 'samashti.board' _description = "Samashti Dashboard" @api.model def get_stock_moves_data(self, from_date, to_date): # Convert string dates to datetime objects from_date_obj = datetime.strptime(from_date, '%Y-%m-%d').date() to_date_obj = datetime.strptime(to_date, '%Y-%m-%d').date() # Get user's timezone user_tz = self.env.user.tz or 'UTC' local_tz = pytz.timezone(user_tz) # Convert local datetime to UTC from_local = local_tz.localize(datetime.combine(from_date_obj, time.min)) to_local = local_tz.localize(datetime.combine(to_date_obj, time.max)) # from_utc = from_local.astimezone(pytz.UTC) # to_utc = to_local.astimezone(pytz.UTC) # Convert to string in Odoo datetime format fromDate = "'" + fields.Datetime.to_string(from_local) + "'" toDate = "'" + fields.Datetime.to_string(to_local) + "'" # Get opening stock at the beginning of the from_date opening_context = { 'lang': 'en_US', 'tz': 'Asia/Kolkata', 'to_date': fields.Datetime.to_string(from_local) } opening_stock = self.env['product.product'].with_context(**opening_context).search_read( [('is_storable', '=', True)], ['id', 'name', 'qty_available'] ) # Create opening stock dictionary for easier lookup opening_dict = {product['id']: product['qty_available'] for product in opening_stock} # Get closing stock at the end of the to_date closing_context = { 'lang': 'en_US', 'tz': 'Asia/Kolkata', 'to_date': fields.Datetime.to_string(to_local) } closing_stock = self.env['product.product'].with_context(**closing_context).search_read( [('is_storable', '=', True)], ['id', 'name', 'qty_available'] ) # Create closing stock dictionary for easier lookup closing_dict = {product['id']: product['qty_available'] for product in closing_stock} sql = f""" WITH stock_movements AS ( SELECT pp.id as product_id, pp.default_code AS product_code, pt.name->>'en_US' AS product_name, pc.name AS category, uom.name->>'en_US' AS uom, -- Current Cost (from Valuation Layer) COALESCE(( SELECT SUM(svl.value) / NULLIF(SUM(svl.quantity), 0) FROM stock_valuation_layer svl WHERE svl.product_id = pp.id ), 0) AS current_cost, -- Receipts: from supplier and inventory adjustments in date range SUM(CASE WHEN sm.date BETWEEN {fromDate} AND {toDate} AND sl_dest.usage = 'internal' AND sl_src.usage = 'supplier' THEN sm.product_uom_qty * (uom.factor / sm_uom.factor) ELSE 0 END) AS receipts, -- Production: internal moves from production SUM(CASE WHEN sm.date BETWEEN {fromDate} AND {toDate} AND sl_dest.usage = 'internal' AND sl_src.usage = 'production' AND sm.production_id IS NOT NULL THEN sm.product_uom_qty * (uom.factor / sm_uom.factor) ELSE 0 END) AS production, -- Consumption: internal moves to production SUM(CASE WHEN sm.date BETWEEN {fromDate} AND {toDate} AND sl_src.usage = 'internal' AND sl_dest.usage = 'production' THEN sm.product_uom_qty * (uom.factor / sm_uom.factor) ELSE 0 END) AS consumption, -- Dispatch: internal moves to customer SUM(CASE WHEN sm.date BETWEEN {fromDate} AND {toDate} AND sl_src.usage = 'internal' AND sl_dest.usage = 'customer' THEN sm.product_uom_qty * (uom.factor / sm_uom.factor) ELSE 0 END) AS dispatch FROM stock_move sm JOIN product_product pp ON sm.product_id = pp.id JOIN product_template pt ON pp.product_tmpl_id = pt.id JOIN product_category pc ON pt.categ_id = pc.id JOIN uom_uom uom ON pt.uom_id = uom.id -- Product default UOM JOIN uom_uom sm_uom ON sm.product_uom = sm_uom.id -- Stock move UOM JOIN stock_location sl_src ON sm.location_id = sl_src.id JOIN stock_location sl_dest ON sm.location_dest_id = sl_dest.id WHERE sl_src.usage IN ('internal', 'supplier', 'production', 'customer', 'inventory') AND sl_dest.usage IN ('internal', 'supplier', 'production', 'customer', 'inventory') AND sm.state = 'done' AND pt.type = 'consu' AND pp.default_code IS NOT NULL AND pp.active = TRUE GROUP BY pp.id, pp.default_code, pt.name, pc.name, uom.name ) SELECT product_id, product_code, product_name, category, uom, current_cost, COALESCE(receipts, 0) AS receipts, COALESCE(production, 0) AS production, COALESCE(consumption, 0) AS consumption, COALESCE(dispatch, 0) AS dispatch FROM stock_movements ORDER BY product_name; """ self.env.cr.execute(sql) data = self.env.cr.dictfetchall() # Process the data processed_data = [] for row in data: product_id = row['product_id'] opening_qty = opening_dict.get(product_id, 0) closing_qty = closing_dict.get(product_id, 0) # Calculate net movement during the period net_movement = ( row['receipts'] + row['production'] - row['consumption'] - row['dispatch'] ) # Verify calculation matches with actual closing stock calculated_closing = opening_qty + net_movement actual_closing = closing_qty processed_row = { 'product_id': product_id, 'product_code': row['product_code'] or '', 'product_name': f"[{row['product_code']}] {row['product_name']}" if row['product_code'] else row[ 'product_name'], 'category': row['category'] or '', 'uom': row['uom'] or '-', 'current_cost': row['current_cost'], 'opening_stock': opening_qty, 'receipts': row['receipts'], 'production': row['production'], 'consumption': row['consumption'], 'dispatch': row['dispatch'], 'calculated_closing_stock': calculated_closing, 'closing_stock': actual_closing, 'variance': actual_closing - calculated_closing, 'value': actual_closing * row['current_cost'] } processed_data.append(processed_row) return processed_data @api.model def get_dashboard_cards_data(self): all_prod = self.env['product.product'].search([('type', '=', 'consu')]) all_category = all_prod.categ_id out_stock_prods = all_prod.filtered(lambda x:x.qty_available <= 0) low_stock_prods = self.env['stock.warehouse.orderpoint'].search([('qty_to_order', '>', 0)]).product_id return { 'products_count': [len(all_prod),all_prod.ids], 'category_count':len(all_category), 'out_stock_prods': [len(out_stock_prods),out_stock_prods.ids], 'low_stock_prods': [len(low_stock_prods),low_stock_prods.ids], } @api.model def get_sale_margin_data(self, from_date, to_date): # Get user's timezone from_date_obj = datetime.strptime(from_date, '%Y-%m-%d').date() to_date_obj = datetime.strptime(to_date, '%Y-%m-%d').date() # Get user's timezone user_tz = self.env.user.tz or 'UTC' local_tz = pytz.timezone(user_tz) # Convert local datetime to UTC from_local = local_tz.localize(datetime.combine(from_date_obj, time.min)) to_local = local_tz.localize(datetime.combine(to_date_obj, time.max)) # from_utc = from_local.astimezone(pytz.UTC) # to_utc = to_local.astimezone(pytz.UTC) # Convert to string in Odoo datetime format from_datetime = fields.Datetime.to_string(from_local) to_datetime = fields.Datetime.to_string(to_local) # Search for posted invoices within the date range invoice_ids = self.env['account.move'].search([ ('state', '=', 'posted'), ('invoice_date', '>=', from_datetime), ('invoice_date', '<=', to_datetime) ]) # Read sale orders linked to those invoices sale_orders = self.env['sale.order'].search_read( [ ('state', '=', 'sale'), ('invoice_ids', 'in', invoice_ids.ids) ], [ 'id', 'name', 'amount_untaxed', 'partner_id', 'total_production_cost', 'date_order', 'order_line', 'freight_charges' ] ) datas = [] for order_data in sale_orders: order = self.env['sale.order'].browse(order_data['id']) cost = order_data['total_production_cost'] or 0.0 sale_price = order_data['amount_untaxed'] or 0.0 margin = sale_price - cost margin_percent = (margin / sale_price * 100) if sale_price else 0.0 customer = order_data['partner_id'][-1] if order_data['partner_id'] else "Unknown" quantity = sum(order.order_line.mapped('product_uom_qty')) weight = sum(order.order_line.mapped('bag_weight')) # Combine product tags product_tags = ', '.join( tag.name for tag in order.order_line.mapped('product_id.all_product_tag_ids') ) # Combine invoice names and dates if order.invoice_ids: invoice = ', '.join(inv.name for inv in order.invoice_ids) date = ', '.join(str(inv.invoice_date.strftime('%d-%m-%Y')) for inv in order.invoice_ids if inv.invoice_date) else: invoice = "N/A" date = "No invoices" datas.append({ 'sale_order': order_data['name'], 'id': order_data['id'], 'weight': weight, 'tags': product_tags, 'invoice': invoice, 'freight':order_data['freight_charges'], 'customer': customer, 'quantity': quantity, 'cost': cost, 'date': date, 'sale_price': sale_price, 'margin': margin, 'margin_percent': margin_percent, }) return datas def action_view_sale_orders(self, id): result = self.env['ir.actions.act_window']._for_xml_id('sale.action_orders') result['views'] = [(self.env.ref('sale.view_order_form', False).id, 'form')] result['res_id'] = id result['target'] = 'self', return result @api.model def get_consumption_data(self, from_date, to_date): from_date_obj = datetime.strptime(from_date, '%Y-%m-%d').date() to_date_obj = datetime.strptime(to_date, '%Y-%m-%d').date() # Get user's timezone user_tz = self.env.user.tz or 'UTC' local_tz = pytz.timezone(user_tz) # Convert local datetime to UTC from_local = local_tz.localize(datetime.combine(from_date_obj, time.min)) to_local = local_tz.localize(datetime.combine(to_date_obj, time.max)) # from_utc = from_local.astimezone(pytz.UTC) # to_utc = to_local.astimezone(pytz.UTC) # Convert to string in Odoo datetime format fromDate = "'"+str(fields.Datetime.to_string(from_local))+"'" toDate = "'"+str(fields.Datetime.to_string(to_local))+"'" mo_ids = self.env['mrp.production'].search([ ('state', '=', 'done'), ('date_start', '>=', fromDate), ('date_start', '<=', toDate) ]) move_ids = mo_ids.move_raw_ids + mo_ids.move_finished_ids + mo_ids.scrap_ids.move_ids stock_layer_ids = move_ids.filtered(lambda x:x.location_id.usage == 'production' or x.location_dest_id.usage == 'production' ).stock_valuation_layer_ids data = [] for l in stock_layer_ids.filtered(lambda x:x.quantity != 0): mo = self.env['mrp.production'].search([('name', 'ilike', l.reference),('state','=','done')]) product_tags = ', '.join( tag.name for tag in l.mapped('product_id.product_tag_ids') ) data.append({ 'product_code': l.product_id.default_code, 'product_name': l.product_id.display_name, 'date':mo.date_start.strftime('%d-%m-%Y'), 'tags':product_tags, 'uom': l.uom_id.name, 'weight': l.quantity * (l.product_id.weight or 1), 'quantity':l.quantity, 'value':l.value, 'reference':l.reference }) return data