From 87f6c16dcf532efd7a61eeb84b931e06be06370d Mon Sep 17 00:00:00 2001 From: Sanjay Jamod Date: Thu, 22 Feb 2018 11:40:14 +0530 Subject: [PATCH] [IMP] mrp_bom_cost: Compute price from BoM Purpose ======= Compute cost of product(s) based on it's BoM Specification ============= real_time: not with FIFO otherwise open the wizard with the bom price as default new value periodic: directly set the cost on the form view Add a server action in order to directly set the price on selected product. When getting price from a bom with a sub-bom it will take the cost of the sub bom except if the sub-bom is set with the bom itself in the server action (in this case the sub bom cost will be used). Capacity is not used for this functionality task-39561 --- addons/mrp/models/mrp_production.py | 5 +- addons/mrp/wizard/change_production_qty.py | 6 +- addons/mrp_bom_cost/__manifest__.py | 9 +- addons/mrp_bom_cost/models/product.py | 91 +++++++++++++-------- addons/mrp_bom_cost/views/product_views.xml | 68 +++++++++------ 5 files changed, 110 insertions(+), 69 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 0f400253a18..337a7798ad0 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -2,12 +2,11 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from collections import defaultdict -import math from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round class MrpProduction(models.Model): """ Manufacturing Orders """ @@ -497,7 +496,7 @@ class MrpProduction(models.Model): for operation in bom.routing_id.operation_ids: # create workorder - cycle_number = math.ceil(bom_qty / operation.workcenter_id.capacity) # TODO: float_round UP + cycle_number = float_round(bom_qty / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP') duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index 90db7f09481..5c3aeb07dd2 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -4,8 +4,8 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import float_is_zero -import math +from odoo.tools import float_is_zero, float_round + class ChangeProductionQty(models.TransientModel): _name = 'change.production.qty' @@ -61,7 +61,7 @@ class ChangeProductionQty(models.TransientModel): for wo in production.workorder_ids: operation = wo.operation_id if operation_bom_qty.get(operation.id): - cycle_number = math.ceil(operation_bom_qty[operation.id] / operation.workcenter_id.capacity) # TODO: float_round UP + cycle_number = float_round(operation_bom_qty[operation.id] / operation.workcenter_id.capacity, precision_digits=0, rounding_method='UP') wo.duration_expected = (operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) diff --git a/addons/mrp_bom_cost/__manifest__.py b/addons/mrp_bom_cost/__manifest__.py index cfddc064353..e1094e5fd33 100644 --- a/addons/mrp_bom_cost/__manifest__.py +++ b/addons/mrp_bom_cost/__manifest__.py @@ -2,14 +2,15 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. { - "name" : "Product extension to track sales and purchases", + "name" : "Product Extension: Compute price from BoM", "version" : "1.0", "depends" : ["mrp", "stock_account"], "category" : "Manufacturing", "description": """ -Product extension. This module adds: - * Computes standard price from the BoM of the product with a button on the product variant based - on the materials in the BoM and the work centers. It can create the necessary accounting entries when necessary. +Allows to compute the standard price of the product based on its BoM, +using the prices of its components and the cost linked to the work centers. +It adds a button on the product itself but also an action in the list view of the products. +If the automated inventory valuation is active, the required accounting entries will be created. """, "init_xml" : [], "demo_xml" : [], diff --git a/addons/mrp_bom_cost/models/product.py b/addons/mrp_bom_cost/models/product.py index 19ef8e08492..c1a276918ae 100644 --- a/addons/mrp_bom_cost/models/product.py +++ b/addons/mrp_bom_cost/models/product.py @@ -1,51 +1,72 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, models +from odoo import api, models, _ +from odoo.exceptions import UserError class ProductTemplate(models.Model): _name = 'product.template' _inherit = 'product.template' - @api.multi - def compute_price(self): - for template in self: - if template.product_variant_count == 1: - return template.product_variant_id.compute_price() + def action_bom_cost(self): + templates = self.filtered(lambda t: t.product_variant_count == 1 and t.bom_count > 0) + return templates.mapped('product_variant_id').action_bom_cost() + + def button_bom_cost(self): + templates = self.filtered(lambda t: t.product_variant_count == 1 and t.bom_count > 0) + return templates.mapped('product_variant_id').button_bom_cost() class ProductProduct(models.Model): _name = 'product.product' _inherit = 'product.product' - @api.multi - def compute_price(self): - bom_obj = self.env['mrp.bom'] - action_rec = self.env.ref('stock_account.action_view_change_standard_price') - for product in self: - bom = bom_obj._bom_find(product=product) - if bom: - price = product._calc_price(bom) - if action_rec: - action = action_rec.read([])[0] - action['context'] = {'default_new_price': price} - return action - return True + def button_bom_cost(self): + self.ensure_one() + price = self._get_price_from_bom() + if self.valuation == 'real_time': + action_rec = self.env.ref('stock_account.action_view_change_standard_price') + action = action_rec.read([])[0] + action['context'] = {'default_new_price': price} + return action + else: + self.standard_price = price - def _calc_price(self, bom): - price = 0.0 - result, result2 = bom.explode(self, 1) - for sbom, sbom_data in result2: - price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty'] - if bom.routing_id: - # FIXME master: remove me - if hasattr(self.env['mrp.workcenter'], 'costs_hour'): - total_cost = 0.0 - for order in bom.routing_id.operation_ids: - total_cost += (order.time_cycle/60) * order.workcenter_id.costs_hour - price += bom.product_uom_id._compute_price(total_cost, bom.product_id.uom_id) - # Convert on product UoM quantities - if price > 0: - price = bom.product_uom_id._compute_price(price / bom.product_qty, self.uom_id) - return price + def action_bom_cost(self): + real_time_products = self.filtered(lambda p: p.valuation == 'real_time') + if real_time_products: + raise UserError(_('The inventory valuation of some products %s is automated. You can only update their cost from the product form.') % (real_time_products.mapped('display_name'))) + + boms_to_recompute = self.env['mrp.bom'].search(['|', ('product_id', 'in', self.ids), '&', ('product_id', '=', False), ('product_tmpl_id', 'in', self.mapped('product_tmpl_id').ids)]) + for product in self: + product.standard_price = product._get_price_from_bom(boms_to_recompute) + + def _get_price_from_bom(self, boms_to_recompute=False): + self.ensure_one() + bom = self.env['mrp.bom']._bom_find(product=self) + return self._compute_bom_price(bom, boms_to_recompute=boms_to_recompute) + + def _compute_bom_price(self, bom, boms_to_recompute=False): + self.ensure_one() + if not boms_to_recompute: + boms_to_recompute = [] + total = 0 + quant_quantity = bom.product_uom_id._compute_quantity(bom.product_qty, bom.product_tmpl_id.uom_id) + for opt in bom.routing_id.operation_ids: + duration_expected = ( + opt.workcenter_id.time_start + + opt.workcenter_id.time_stop + + quant_quantity * opt.time_cycle) + total += (duration_expected / 60) * opt.workcenter_id.costs_hour + for line in bom.bom_line_ids: + if line._skip_bom_line(self): + continue + + # Compute recursive if line has `child_line_ids` + if line.child_bom_id and line.child_bom_id in boms_to_recompute: + child_total = line.product_id._compute_bom_price(line.child_bom_id, boms_to_recompute=boms_to_recompute) + total += line.product_id.uom_id._compute_price(child_total, line.product_uom_id) * line.product_qty + else: + total += line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line.product_qty + return bom.product_uom_id._compute_price(total / bom.product_qty, self.uom_id) diff --git a/addons/mrp_bom_cost/views/product_views.xml b/addons/mrp_bom_cost/views/product_views.xml index 2b7e4eca022..c51312ea255 100644 --- a/addons/mrp_bom_cost/views/product_views.xml +++ b/addons/mrp_bom_cost/views/product_views.xml @@ -7,14 +7,13 @@ -
-
+ class="oe_link oe_read_only"/> +
@@ -25,34 +24,55 @@ -
-
+
- - product.product.view.form.easy.inherit.extended + + product.product.product.view.form.easy.bom.inherit product.product - - - - -