# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict from odoo import api, fields, models from odoo.tools import float_round class MrpCostStructure(models.AbstractModel): _name = 'report.mrp_account_enterprise.mrp_cost_structure' _description = 'MRP Cost Structure Report' def get_lines(self, productions): ProductProduct = self.env['product.product'] StockMove = self.env['stock.move'] res = [] currency_table = self.env['res.currency']._get_query_currency_table(self.env.companies.ids, fields.Date.today()) for product in productions.mapped('product_id'): mos = productions.filtered(lambda m: m.product_id == product) # variables to calc cost share (i.e. between products/byproducts) since MOs can have varying distributions total_cost_by_mo = defaultdict(float) component_cost_by_mo = defaultdict(float) operation_cost_by_mo = defaultdict(float) # Get operations details + cost operations = [] total_cost_operations = 0.0 Workorders = self.env['mrp.workorder'].search([('production_id', 'in', mos.ids)]) if Workorders: total_cost_operations = self._compute_mo_operation_cost(currency_table, Workorders, total_cost_by_mo, operation_cost_by_mo, total_cost_operations, operations) # Get the cost of raw material effectively used raw_material_moves = {} total_cost_components = 0.0 query_str = """SELECT sm.product_id, mo.id, abs(SUM(svl.quantity)), abs(SUM(svl.value)), currency_table.rate FROM stock_move AS sm INNER JOIN stock_valuation_layer AS svl ON svl.stock_move_id = sm.id LEFT JOIN mrp_production AS mo on sm.raw_material_production_id = mo.id LEFT JOIN {currency_table} ON currency_table.company_id = mo.company_id WHERE sm.raw_material_production_id in %s AND sm.state != 'cancel' AND sm.product_qty != 0 AND scrapped != 't' GROUP BY sm.product_id, mo.id, currency_table.rate""".format(currency_table=currency_table,) self.env.cr.execute(query_str, (tuple(mos.ids), )) for product_id, mo_id, qty, cost, currency_rate in self.env.cr.fetchall(): cost *= currency_rate if product_id in raw_material_moves: product_moves = raw_material_moves[product_id] product_moves['cost'] += cost product_moves['qty'] += qty else: raw_material_moves[product_id] = { 'qty': qty, 'cost': cost, 'product_id': ProductProduct.browse(product_id), } total_cost_by_mo[mo_id] += cost component_cost_by_mo[mo_id] += cost total_cost_components += cost raw_material_moves = list(raw_material_moves.values()) # Get the cost of scrapped materials scraps = StockMove.search([('production_id', 'in', mos.ids), ('scrapped', '=', True), ('state', '=', 'done')]) # Get the byproducts and their total + avg per uom cost share amounts total_cost_by_product = defaultdict(float) qty_by_byproduct = defaultdict(float) qty_by_byproduct_w_costshare = defaultdict(float) component_cost_by_product = defaultdict(float) operation_cost_by_product = defaultdict(float) # tracking consistent uom usage across each byproduct when not using byproduct's product uom is too much of a pain # => calculate byproduct qtys/cost in same uom + cost shares (they are MO dependent) byproduct_moves = mos.move_byproduct_ids.filtered(lambda m: m.state != 'cancel') for move in byproduct_moves: qty_by_byproduct[move.product_id] += move.product_uom._compute_quantity(move.quantity, move.product_id.uom_id, rounding_method='HALF-UP') # byproducts w/o cost share shouldn't be included in cost breakdown if move.cost_share != 0: qty_by_byproduct_w_costshare[move.product_id] += move.product_uom._compute_quantity(move.quantity, move.product_id.uom_id, rounding_method='HALF-UP') cost_share = move.cost_share / 100 total_cost_by_product[move.product_id] += total_cost_by_mo[move.production_id.id] * cost_share component_cost_by_product[move.product_id] += component_cost_by_mo[move.production_id.id] * cost_share operation_cost_by_product[move.product_id] += operation_cost_by_mo[move.production_id.id] * cost_share # Get product qty and its relative total + avg per uom cost share amount uom = product.uom_id mo_qty = 0 for m in mos: cost_share = float_round(1 - sum(m.move_finished_ids.mapped('cost_share')) / 100, precision_rounding=0.0001) total_cost_by_product[product] += total_cost_by_mo[m.id] * cost_share component_cost_by_product[product] += component_cost_by_mo[m.id] * cost_share operation_cost_by_product[product] += operation_cost_by_mo[m.id] * cost_share mo_qty += sum(m.move_finished_ids.filtered(lambda mo: mo.state == 'done' and mo.product_id == product).mapped('quantity')) res.append({ 'product': product, 'mo_qty': mo_qty, 'mo_uom': uom, 'operations': operations, 'currency': self.env.company.currency_id, 'raw_material_moves': raw_material_moves, 'total_cost_components': total_cost_components, 'total_cost_operations': total_cost_operations, 'total_cost': total_cost_components + total_cost_operations, 'scraps': scraps, 'mocount': len(mos), 'byproduct_moves': byproduct_moves, 'component_cost_by_product': component_cost_by_product, 'operation_cost_by_product': operation_cost_by_product, 'qty_by_byproduct': qty_by_byproduct, 'qty_by_byproduct_w_costshare': qty_by_byproduct_w_costshare, 'total_cost_by_product': total_cost_by_product }) return res @api.model def _get_report_values(self, docids, data=None): productions = self.env['mrp.production']\ .browse(docids)\ .filtered(lambda p: p.state != 'cancel') res = None if all(production.state == 'done' for production in productions): res = self.get_lines(productions) return {'lines': res} def _compute_mo_operation_cost(self, currency_table, Workorders, total_cost_by_mo, operation_cost_by_mo, total_cost_operations, operations): query_str = """ SELECT wo.production_id, wo.id, op.id, wo.name, wc.name, wo.duration, CASE WHEN wo.costs_hour = 0.0 THEN wc.costs_hour ELSE wo.costs_hour END AS costs_hour, currency_table.rate FROM mrp_workcenter_productivity t LEFT JOIN mrp_workorder wo ON (wo.id = t.workorder_id) LEFT JOIN mrp_workcenter wc ON (wc.id = t.workcenter_id) LEFT JOIN mrp_routing_workcenter op ON (wo.operation_id = op.id) LEFT JOIN {currency_table} ON currency_table.company_id = t.company_id WHERE t.workorder_id IS NOT NULL AND t.workorder_id IN %s GROUP BY wo.production_id, wo.id, op.id, wo.name, wc.costs_hour, wc.name, currency_table.rate ORDER BY wo.name, wc.name """.format(currency_table=currency_table,) self.env.cr.execute(query_str, (tuple(Workorders.ids), )) for mo_id, dummy_wo_id, op_id, wo_name, wc_name, duration, cost_hour, currency_rate in self.env.cr.fetchall(): cost = duration / 60.0 * cost_hour * currency_rate total_cost_by_mo[mo_id] += cost operation_cost_by_mo[mo_id] += cost total_cost_operations += cost operations.append([wc_name, op_id, wo_name, duration / 60.0, cost_hour * currency_rate]) return total_cost_operations class ProductTemplateCostStructure(models.AbstractModel): _name = 'report.mrp_account_enterprise.product_template_cost_structure' _description = 'Product Template Cost Structure Report' @api.model def _get_report_values(self, docids, data=None): productions = self.env['mrp.production'].search([('product_id', 'in', docids), ('state', '=', 'done')]) res = self.env['report.mrp_account_enterprise.mrp_cost_structure'].get_lines(productions) return {'lines': res}