[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
This commit is contained in:
Sanjay Jamod
2018-04-09 15:39:19 +02:00
committed by Arnold Moyaux
parent 8f268469d5
commit 87f6c16dcf
5 changed files with 110 additions and 69 deletions
+2 -3
View File
@@ -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)
+3 -3
View File
@@ -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)
+5 -4
View File
@@ -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" : [],
+56 -35
View File
@@ -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)
+44 -24
View File
@@ -7,14 +7,13 @@
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
<field name="arch" type="xml">
<div name="standard_price_uom" position="after">
<button name="compute_price"
string="Compute from BOM" type="object"
attrs="{'invisible': ['|', '|', ('cost_method', '!=', 'standard'), ('bom_count', '=', 0), ('product_variant_count', '!=', 1)]}"
<xpath expr="//div[@name='update_cost_price']" position="inside">
<button name="button_bom_cost"
string="Compute Price from BoM" type="object"
attrs="{'invisible': ['|', ('bom_count', '=', 0), '&amp;', ('valuation', '=', 'real_time'), ('cost_method', '=', 'fifo')]}"
help="Compute the price of the product using products and operations of related bill of materials, for manufactured products only."
class="oe_link oe_read_only"
colspan="2"/>
</div>
class="oe_link oe_read_only"/>
</xpath>
</field>
</record>
@@ -25,34 +24,55 @@
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
<field name="arch" type="xml">
<div name="standard_price_uom" position="after">
<button name="compute_price"
string="Compute from BOM" type="object"
attrs="{'invisible': ['|', ('cost_method', '!=', 'standard'), ('bom_count', '=', 0)]}"
<xpath expr="//div[@name='update_cost_price']" position="inside">
<button name="button_bom_cost"
string="Compute Price from BoM" type="object"
attrs="{'invisible': ['|', ('bom_count', '=', 0), '&amp;', ('valuation', '=', 'real_time'), ('cost_method', '=', 'fifo')]}"
help="Compute the price of the product using products and operations of related bill of materials, for manufactured products only."
class="oe_link oe_read_only"
colspan="2"/>
</div>
</xpath>
</field>
</record>
<record id="product_product_view_form_easy_inherit_extended" model="ir.ui.view">
<field name="name">product.product.view.form.easy.inherit.extended</field>
<record id="product_variant_easy_edit_view_bom_inherit" model="ir.ui.view">
<field name="name">product.product.product.view.form.easy.bom.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
<field name="arch" type="xml">
<field name="standard_price" position="after">
<field name="cost_method" invisible='1'/>
<field name="bom_count" invisible='1'/>
<button name="compute_price"
string="Compute from BOM" type="object"
attrs="{'invisible': ['|', ('cost_method', '!=', 'standard'), ('bom_count', '=', 0)]}"
<data>
<xpath expr="//div[@name='update_cost_price']" position="inside">
<field name="bom_count" invisible="1"/>
<button name="button_bom_cost"
string="Compute Price from BoM"
type="object"
attrs="{'invisible': ['|', ('bom_count', '=', 0), '&amp;', ('valuation', '=', 'real_time'), ('cost_method', '=', 'fifo')]}"
help="Compute the price of the product using products and operations of related bill of materials, for manufactured products only."
class="oe_link oe_read_only"
colspan="2"/>
</field>
class="oe_link oe_read_only"/>
</xpath>
</data>
</field>
</record>
<record id="action_compute_price_bom_template" model="ir.actions.server">
<field name="name">Compute Price from BoM</field>
<field name="model_id" ref="product.model_product_template"/>
<field name="binding_model_id" ref="product.model_product_template"/>
<field name="state">code</field>
<field name="code">
if records:
records.action_bom_cost()
</field>
</record>
<record id="action_compute_price_bom_product" model="ir.actions.server">
<field name="name">Compute Price from BoM</field>
<field name="model_id" ref="product.model_product_product"/>
<field name="binding_model_id" ref="product.model_product_product"/>
<field name="state">code</field>
<field name="code">
if records:
records.action_bom_cost()
</field>
</record>
</odoo>