odoo18/custom_addons/dashboard/models/stock_dashboard.py

278 lines
13 KiB
Python

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):
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_utc))+"'"
toDate = "'"+str(fields.Datetime.to_string(to_utc))+"'"
sql = f"""
SELECT
pp.default_code AS product_code,
pt.name AS product_name,
pc.name AS category,
uom.name 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,
-- Opening Stock: includes inventory adjustments before fromDate
COALESCE(SUM(CASE
WHEN sm.date < {fromDate} AND sl_dest.usage = 'internal' THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
WHEN sm.date < {fromDate} AND sl_src.usage = 'internal' THEN -sm.product_uom_qty * (uom.factor /sm_uom.factor)
WHEN sm.date < {fromDate} AND (sl_src.usage = 'inventory' OR sl_dest.usage = 'inventory') THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
ELSE 0
END), 0) AS opening_stock,
-- Receipts: from supplier and inventory adjustments in date range
COALESCE(SUM(CASE
WHEN sm.date BETWEEN {fromDate} AND {toDate}
AND sl_dest.usage = 'internal'
AND sl_src.usage in ('supplier','inventory') THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
WHEN sm.date BETWEEN {fromDate} AND {toDate} AND (sl_src.usage = 'inventory' OR sl_dest.usage = 'inventory') THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
ELSE 0
END), 0) AS receipts,
-- Production: internal moves from production
COALESCE(SUM(CASE
WHEN sm.date BETWEEN {fromDate} AND {toDate}
AND sl_dest.usage = 'internal'
AND sl_src.usage = 'production' THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
ELSE 0
END), 0) AS production,
-- Consumption: internal moves to production
COALESCE(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), 0) AS consumption,
-- Dispatch: internal moves to customer
COALESCE(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), 0) AS dispatch,
-- Closing Stock = Opening + Receipts + Production - Consumption - Dispatch + Inventory adjustments
(
COALESCE(SUM(CASE
WHEN sm.date < {fromDate} AND sl_dest.usage = 'internal' THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
WHEN sm.date < {fromDate} AND sl_src.usage = 'internal' THEN -sm.product_uom_qty * (uom.factor /sm_uom.factor)
WHEN sm.date < {fromDate} AND (sl_src.usage = 'inventory' OR sl_dest.usage = 'inventory') THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
WHEN sm.date BETWEEN {fromDate} AND {toDate} AND sl_dest.usage = 'internal' AND sl_src.usage IN ('supplier', 'production') THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
WHEN sm.date BETWEEN {fromDate} AND {toDate} AND (sl_src.usage = 'inventory' OR sl_dest.usage = 'inventory') THEN sm.product_uom_qty * (uom.factor /sm_uom.factor)
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)
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), 0)
) AS closing_stock
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'
GROUP BY
pp.default_code, pt.name, pc.name, uom.name, pp.id
ORDER BY
pt.name;
"""
self.env.cr.execute(sql)
data = self.env.cr.dictfetchall()
if data:
for row in data:
row['product_name'] = '[' + row['product_code'] + '] ' + list(row['product_name'].values())[0] if row[
'product_name'] else ''
row['uom'] = list(row['uom'].values())[0] if row['uom'] else '-'
row['value'] = row['closing_stock'] * row['current_cost']
return data
else:
return []
@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_utc)
to_datetime = fields.Datetime.to_string(to_utc)
# 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'
]
)
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 = f"{sum(order.order_line.mapped('bag_weight'))} kg"
# 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,
'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_utc))+"'"
toDate = "'"+str(fields.Datetime.to_string(to_utc))+"'"
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:
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,
'quantity':l.quantity,
'value':l.value,
'reference':l.reference
})
return data