356 lines
14 KiB
Python
356 lines
14 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):
|
|
# 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
|
|
|
|
|
|
|