[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:
committed by
Arnold Moyaux
parent
8f268469d5
commit
87f6c16dcf
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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" : [],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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), '&', ('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), '&', ('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), '&', ('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>
|
||||
|
||||
Reference in New Issue
Block a user