[IMP] mrp: improve bom filter line by variant feature

On BoM, a component line can be filter out depending
of the product variant to manufacture
(with "Apply on Variant" field).

Extend this feature to operation and byproduct line.

task-2614126

closes odoo/odoo#74973

Signed-off-by: William Henrotin <Whenrow@users.noreply.github.com>
This commit is contained in:
Rémy Voet (ryv)
2021-08-16 07:20:03 +00:00
parent e2f59a6dd1
commit 7b7f65d504
12 changed files with 212 additions and 79 deletions
+51 -41
View File
@@ -6,7 +6,6 @@ from odoo.exceptions import UserError, ValidationError
from odoo.osv.expression import AND, NEGATIVE_TERM_OPERATORS
from odoo.tools import float_round
from itertools import groupby
from collections import defaultdict
@@ -76,18 +75,31 @@ class MrpBom(models.Model):
string='Flexible Consumption',
required=True
)
possible_product_template_attribute_value_ids = fields.Many2many(
'product.template.attribute.value',
compute='_compute_possible_product_template_attribute_value_ids')
_sql_constraints = [
('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'),
]
@api.onchange('product_id')
def onchange_product_id(self):
if self.product_id:
for line in self.bom_line_ids:
line.bom_product_template_attribute_value_ids = False
@api.depends(
'product_tmpl_id.attribute_line_ids.value_ids',
'product_tmpl_id.attribute_line_ids.attribute_id.create_variant',
'product_tmpl_id.attribute_line_ids.product_template_value_ids.ptav_active',
)
def _compute_possible_product_template_attribute_value_ids(self):
for bom in self:
bom.possible_product_template_attribute_value_ids = bom.product_tmpl_id.valid_product_template_attribute_line_ids._without_no_variant_attributes().product_template_value_ids._only_active()
@api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids')
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
self.bom_line_ids.bom_product_template_attribute_value_ids = False
self.operation_ids.bom_product_template_attribute_value_ids = False
self.byproduct_ids.bom_product_template_attribute_value_ids = False
@api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids', 'byproduct_ids', 'operation_ids')
def _check_bom_lines(self):
for bom in self:
for bom_line in bom.bom_line_ids:
@@ -97,17 +109,17 @@ class MrpBom(models.Model):
same_product = bom.product_tmpl_id == bom_line.product_id.product_tmpl_id
if same_product:
raise ValidationError(_("BoM line product %s should not be the same as BoM product.") % bom.display_name)
if bom.product_id and bom_line.bom_product_template_attribute_value_ids:
raise ValidationError(_("BoM cannot concern product %s and have a line with attributes (%s) at the same time.")
% (bom.product_id.display_name, ", ".join([ptav.display_name for ptav in bom_line.bom_product_template_attribute_value_ids])))
for ptav in bom_line.bom_product_template_attribute_value_ids:
if ptav.product_tmpl_id != bom.product_tmpl_id:
raise ValidationError(_(
"The attribute value %(attribute)s set on product %(product)s does not match the BoM product %(bom_product)s.",
attribute=ptav.display_name,
product=ptav.product_tmpl_id.display_name,
bom_product=bom_line.parent_product_tmpl_id.display_name
))
apply_variants = bom.bom_line_ids.bom_product_template_attribute_value_ids | bom.operation_ids.bom_product_template_attribute_value_ids | bom.byproduct_ids.bom_product_template_attribute_value_ids
if bom.product_id and apply_variants:
raise ValidationError(_("You cannot use the 'Apply on Variant' functionality and simultaneously create a BoM for a specific variant."))
for ptav in apply_variants:
if ptav.product_tmpl_id != bom.product_tmpl_id:
raise ValidationError(_(
"The attribute value %(attribute)s set on product %(product)s does not match the BoM product %(bom_product)s.",
attribute=ptav.display_name,
product=ptav.product_tmpl_id.display_name,
bom_product=bom_line.parent_product_tmpl_id.display_name
))
@api.onchange('product_uom_id')
def onchange_product_uom_id(self):
@@ -125,8 +137,9 @@ class MrpBom(models.Model):
self.product_uom_id = self.product_tmpl_id.uom_id.id
if self.product_id.product_tmpl_id != self.product_tmpl_id:
self.product_id = False
for line in self.bom_line_ids:
line.bom_product_template_attribute_value_ids = False
self.bom_line_ids.bom_product_template_attribute_value_ids = False
self.operation_ids.bom_product_template_attribute_value_ids = False
self.byproduct_ids.bom_product_template_attribute_value_ids = False
def copy(self, default=None):
res = super().copy(default)
@@ -327,7 +340,7 @@ class MrpBomLine(models.Model):
'mrp.bom', 'Parent BoM',
index=True, ondelete='cascade', required=True)
parent_product_tmpl_id = fields.Many2one('product.template', 'Parent Product Template', related='bom_id.product_tmpl_id')
possible_bom_product_template_attribute_value_ids = fields.Many2many('product.template.attribute.value', compute='_compute_possible_bom_product_template_attribute_value_ids')
possible_bom_product_template_attribute_value_ids = fields.Many2many(related='bom_id.possible_product_template_attribute_value_ids')
bom_product_template_attribute_value_ids = fields.Many2many(
'product.template.attribute.value', string="Apply on Variants", ondelete='restrict',
domain="[('id', 'in', possible_bom_product_template_attribute_value_ids)]",
@@ -350,15 +363,6 @@ class MrpBomLine(models.Model):
'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'),
]
@api.depends(
'parent_product_tmpl_id.attribute_line_ids.value_ids',
'parent_product_tmpl_id.attribute_line_ids.attribute_id.create_variant',
'parent_product_tmpl_id.attribute_line_ids.product_template_value_ids.ptav_active',
)
def _compute_possible_bom_product_template_attribute_value_ids(self):
for line in self:
line.possible_bom_product_template_attribute_value_ids = line.parent_product_tmpl_id.valid_product_template_attribute_line_ids._without_no_variant_attributes().product_template_value_ids._only_active()
@api.depends('product_id', 'bom_id')
def _compute_child_bom_id(self):
for line in self:
@@ -406,20 +410,12 @@ class MrpBomLine(models.Model):
def _skip_bom_line(self, product):
""" Control if a BoM line should be produced, can be inherited to add
custom control. It currently checks that all variant values are in the
product.
If multiple values are encoded for the same attribute line, only one of
them has to be found on the variant.
custom control.
"""
self.ensure_one()
if product._name == 'product.template':
return False
if self.bom_product_template_attribute_value_ids:
for ptal, iter_ptav in groupby(self.bom_product_template_attribute_value_ids.sorted('attribute_line_id'), lambda ptav: ptav.attribute_line_id):
if not any(ptav in product.product_template_attribute_value_ids for ptav in iter_ptav):
return True
return False
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
def action_see_attachments(self):
domain = [
@@ -464,9 +460,23 @@ class MrpByProduct(models.Model):
operation_id = fields.Many2one(
'mrp.routing.workcenter', 'Produced in Operation', check_company=True,
domain="[('id', 'in', allowed_operation_ids)]")
possible_bom_product_template_attribute_value_ids = fields.Many2many(related='bom_id.possible_product_template_attribute_value_ids')
bom_product_template_attribute_value_ids = fields.Many2many(
'product.template.attribute.value', string="Apply on Variants", ondelete='restrict',
domain="[('id', 'in', possible_bom_product_template_attribute_value_ids)]",
help="BOM Product Variants needed to apply this line.")
@api.onchange('product_id')
def onchange_product_id(self):
def _onchange_product_id(self):
""" Changes UoM if product_id changes. """
if self.product_id:
self.product_uom_id = self.product_id.uom_id.id
def _skip_byproduct_line(self, product):
""" Control if a byproduct line should be produced, can be inherited to add
custom control.
"""
self.ensure_one()
if product._name == 'product.template':
return False
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
+13 -8
View File
@@ -461,7 +461,7 @@ class MrpProduction(models.Model):
relevant_move_state = production.move_raw_ids._get_relevant_state_among_moves()
# Compute reservation state according to its component's moves.
if relevant_move_state == 'partially_available':
if production.bom_id.operation_ids and production.bom_id.ready_to_produce == 'asap':
if production.workorder_ids.operation_id and production.bom_id.ready_to_produce == 'asap':
production.reservation_state = production._get_ready_to_produce_state()
else:
production.reservation_state = 'confirmed'
@@ -602,7 +602,7 @@ class MrpProduction(models.Model):
# we need to avoid keeping incorrect lines, so clearing is necessary too.
if self.product_id != self._origin.product_id:
self.move_raw_ids = [(5,)]
if self.bom_id and self.product_qty > 0:
if self.bom_id and self.product_id and self.product_qty > 0:
# keep manual entries
list_move_raw = [(4, move.id) for move in self.move_raw_ids.filtered(lambda m: not m.bom_line_id)]
moves_raw_values = self._get_moves_raw_values()
@@ -671,9 +671,9 @@ class MrpProduction(models.Model):
if message:
return {'warning': {'title': _('Warning'), 'message': message}}
@api.onchange('bom_id')
@api.onchange('bom_id', 'product_id')
def _onchange_workorder_ids(self):
if self.bom_id:
if self.bom_id and self.product_id:
self._create_workorder()
else:
self.workorder_ids = False
@@ -719,7 +719,7 @@ class MrpProduction(models.Model):
if 'qty_producing' in vals:
finished_move_lines.write({'qty_done': vals.get('qty_producing')})
if not production.bom_id.operation_ids and vals.get('date_planned_start') and not vals.get('date_planned_finished'):
if not production.workorder_ids.operation_id and vals.get('date_planned_start') and not vals.get('date_planned_finished'):
new_date_planned_start = fields.Datetime.to_datetime(vals.get('date_planned_start'))
if not production.date_planned_finished or new_date_planned_start >= production.date_planned_finished:
production.date_planned_finished = new_date_planned_start + datetime.timedelta(hours=1)
@@ -803,7 +803,7 @@ class MrpProduction(models.Model):
def _create_workorder(self):
for production in self:
if not production.bom_id:
if not production.bom_id or not production.product_id:
continue
workorders_values = []
@@ -815,6 +815,8 @@ class MrpProduction(models.Model):
if not (bom.operation_ids and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.operation_ids != bom.operation_ids)):
continue
for operation in bom.operation_ids:
if operation._skip_operation_line(bom_data['product']):
continue
workorders_values += [{
'name': operation.name,
'production_id': production.id,
@@ -864,6 +866,8 @@ class MrpProduction(models.Model):
raise UserError(_("You cannot have %s as the finished product and in the Byproducts", self.product_id.name))
moves.append(production._get_move_finished_values(production.product_id.id, production.product_qty, production.product_uom_id.id))
for byproduct in production.bom_id.byproduct_ids:
if byproduct._skip_byproduct_line(production.product_id):
continue
product_uom_factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id)
qty = byproduct.product_qty * (product_uom_factor / production.bom_id.product_qty)
moves.append(production._get_move_finished_values(
@@ -979,10 +983,11 @@ class MrpProduction(models.Model):
the first operation of the bom. If not returns 'waiting'
"""
self.ensure_one()
first_operation = self.bom_id.operation_ids[0]
if len(self.bom_id.operation_ids) == 1:
operations = self.workorder_ids.operation_id
if len(operations) == 1:
moves_in_first_operation = self.move_raw_ids
else:
first_operation = operations[0]
moves_in_first_operation = self.move_raw_ids.filtered(lambda move: move.operation_id == first_operation)
moves_in_first_operation = moves_in_first_operation.filtered(
lambda move: move.bom_line_id and
+14
View File
@@ -43,6 +43,11 @@ class MrpRoutingWorkcenter(models.Model):
time_cycle = fields.Float('Duration', compute="_compute_time_cycle")
workorder_count = fields.Integer("# Work Orders", compute="_compute_workorder_count")
workorder_ids = fields.One2many('mrp.workorder', 'operation_id', string="Work Orders")
possible_bom_product_template_attribute_value_ids = fields.Many2many(related='bom_id.possible_product_template_attribute_value_ids')
bom_product_template_attribute_value_ids = fields.Many2many(
'product.template.attribute.value', string="Apply on Variants", ondelete='restrict',
domain="[('id', 'in', possible_bom_product_template_attribute_value_ids)]",
help="BOM Product Variants needed to apply this line.")
@api.depends('time_mode', 'time_mode_batch')
def _compute_time_computed_on(self):
@@ -79,3 +84,12 @@ class MrpRoutingWorkcenter(models.Model):
bom_id = self.env.context.get('bom_id')
for operation in self:
operation.copy({'name': _("%s (copy)", operation.name), 'bom_id': bom_id})
def _skip_operation_line(self, product):
""" Control if a operation should be processed, can be inherited to add
custom control.
"""
self.ensure_one()
if product._name == 'product.template':
return False
return not product._match_all_variant_values(self.bom_product_template_attribute_value_ids)
+2
View File
@@ -208,6 +208,8 @@ class MrpUnbuild(models.Model):
factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
moves += unbuild._generate_move_from_bom_line(self.product_id, self.product_uom_id, unbuild.product_qty)
for byproduct in unbuild.bom_id.byproduct_ids:
if byproduct._skip_byproduct_line(unbuild.product_id):
continue
quantity = byproduct.product_qty * factor
moves += unbuild._generate_move_from_bom_line(byproduct.product_id, byproduct.product_uom_id, quantity, byproduct_id=byproduct.id)
return moves
+17
View File
@@ -2,6 +2,8 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from itertools import groupby
from odoo import api, fields, models
from odoo.tools.float_utils import float_round, float_is_zero
@@ -203,3 +205,18 @@ class ProductProduct(models.Model):
res['context']['single_product'] = False
res['context'].pop('default_product_tmpl_id', None)
return res
def _match_all_variant_values(self, product_template_attribute_value_ids):
""" It currently checks that all variant values (`product_template_attribute_value_ids`)
are in the product (`self`).
If multiple values are encoded for the same attribute line, only one of
them has to be found on the variant.
"""
self.ensure_one()
if not product_template_attribute_value_ids:
return True
for _, iter_ptav in groupby(product_template_attribute_value_ids.sorted('attribute_line_id'), lambda ptav: ptav.attribute_line_id):
if not any(ptav in self.product_template_attribute_value_ids for ptav in iter_ptav):
return False
return True
@@ -54,9 +54,10 @@ class ReportBomStructure(models.AbstractModel):
return self.env.ref('mrp.report_mrp_bom_line')._render({'data': lines})
@api.model
def get_operations(self, bom_id=False, qty=0, level=0):
def get_operations(self, product_id=False, bom_id=False, qty=0, level=0):
bom = self.env['mrp.bom'].browse(bom_id)
lines = self._get_operation_line(bom, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
product = self.env['product.product'].browse(product_id)
lines = self._get_operation_line(product, bom, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
values = {
'bom_id': bom_id,
'currency': self.env.company.currency_id,
@@ -110,7 +111,7 @@ class ReportBomStructure(models.AbstractModel):
# Use the product template instead of the variant
price = bom.product_tmpl_id.uom_id._compute_price(bom.product_tmpl_id.with_company(company).standard_price, bom.product_uom_id) * bom_quantity
attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', bom.product_tmpl_id.id)])
operations = self._get_operation_line(bom, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
operations = self._get_operation_line(product, bom, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
lines = {
'bom': bom,
'bom_qty': bom_quantity,
@@ -166,10 +167,12 @@ class ReportBomStructure(models.AbstractModel):
total += sub_total
return components, total
def _get_operation_line(self, bom, qty, level):
def _get_operation_line(self, product, bom, qty, level):
operations = []
total = 0.0
for operation in bom.operation_ids:
if operation._skip_operation_line(product):
continue
operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP')
duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
@@ -192,7 +195,7 @@ class ReportBomStructure(models.AbstractModel):
# will be the 5 for a quantity between 1-10, then doubled for
# 11-20,...
operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP')
operations = self._get_operation_line(bom, operation_cycle, 0)
operations = self._get_operation_line(product, bom, operation_cycle, 0)
price += sum([op['total'] for op in operations])
for line in bom.bom_line_ids:
@@ -126,7 +126,7 @@
</t>
<t t-if="data['operations']">
<t t-set="space_td" t-value="'margin-left: '+ str(data['level'] * 20) + 'px;'"/>
<tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-data-id="'operation-' + str(data['bom'].id)" t-att-data-bom-id="data['bom'].id" t-att-parent_id="data['bom'].id" t-att-data-qty="data['bom_qty']" t-att-data-level="data['level']">
<tr class="o_mrp_bom_report_line o_mrp_bom_cost" t-att-data-product_id="data['product'].id" t-att-data-id="'operation-' + str(data['bom'].id)" t-att-data-bom-id="data['bom'].id" t-att-parent_id="data['bom'].id" t-att-data-qty="data['bom_qty']" t-att-data-level="data['level']">
<td name="td_opr">
<span t-att-style="space_td"/>
<span class="o_mrp_bom_unfoldable fa fa-fw fa-caret-right" t-att-data-function="'get_operations'" role="img" aria-label="Unfold" title="Unfold"/>
@@ -86,11 +86,13 @@ var MrpBomReport = stock_report_generic.extend({
var $parent = $(event.currentTarget).closest('tr');
var activeID = $parent.data('bom-id');
var qty = $parent.data('qty');
var productId = $parent.data('product_id');
var level = $parent.data('level') || 0;
return this._rpc({
model: 'report.mrp.report_bom_structure',
method: 'get_operations',
args: [
productId,
activeID,
parseFloat(qty),
level + 1
+88 -23
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import exceptions
from odoo import exceptions, Command
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tools import float_compare, float_round
@@ -22,35 +22,69 @@ class TestBoM(TestMrpCommon):
def test_10_variants(self):
test_bom = self.env['mrp.bom'].create({
'product_id': self.product_7_3.id,
'product_tmpl_id': self.product_7_template.id,
'product_uom_id': self.uom_unit.id,
'product_qty': 4.0,
'type': 'normal',
})
test_bom.write({
'operation_ids': [
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
(0, 0, {'name': 'Weld Machine', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 18, 'sequence': 2}),
Command.create({
'name': 'Cutting Machine',
'workcenter_id': self.workcenter_1.id,
'time_cycle': 12,
'sequence': 1
}),
Command.create({
'name': 'Weld Machine',
'workcenter_id': self.workcenter_1.id,
'time_cycle': 18,
'sequence': 2,
'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v1.id)]
}),
Command.create({
'name': 'Taking a coffee',
'workcenter_id': self.workcenter_1.id,
'time_cycle': 5,
'sequence': 3,
'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v2.id)]
})
],
'byproduct_ids': [
Command.create({
'product_id': self.product_1.id,
'product_uom_id': self.product_1.uom_id.id,
'product_qty': 1,
}),
Command.create({
'product_id': self.product_2.id,
'product_uom_id': self.product_2.uom_id.id,
'product_qty': 1,
'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v1.id)]
}),
Command.create({
'product_id': self.product_3.id,
'product_uom_id': self.product_3.uom_id.id,
'product_qty': 1,
'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v2.id)]
}),
],
'bom_line_ids': [
Command.create({
'product_id': self.product_2.id,
'product_qty': 2,
}),
Command.create({
'product_id': self.product_3.id,
'product_qty': 2,
'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v1.id)],
}),
Command.create({
'product_id': self.product_4.id,
'product_qty': 2,
'bom_product_template_attribute_value_ids': [Command.link(self.product_7_attr1_v2.id)],
}),
]
})
test_bom_l1 = self.env['mrp.bom.line'].create({
'bom_id': test_bom.id,
'product_id': self.product_2.id,
'product_qty': 2,
})
test_bom_l2 = self.env['mrp.bom.line'].create({
'bom_id': test_bom.id,
'product_id': self.product_3.id,
'product_qty': 2,
'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v1.id)],
})
test_bom_l3 = self.env['mrp.bom.line'].create({
'bom_id': test_bom.id,
'product_id': self.product_4.id,
'product_qty': 2,
'bom_product_template_attribute_value_ids': [(4, self.product_7_attr1_v2.id)],
})
test_bom_l1, test_bom_l2, test_bom_l3 = test_bom.bom_line_ids
boms, lines = test_bom.explode(self.product_7_3, 4)
self.assertIn(test_bom, [b[0]for b in boms])
self.assertIn(test_bom_l1, [l[0] for l in lines])
@@ -69,6 +103,37 @@ class TestBoM(TestMrpCommon):
self.assertNotIn(test_bom_l2, [l[0] for l in lines])
self.assertIn(test_bom_l3, [l[0] for l in lines])
mrp_order_form = Form(self.env['mrp.production'])
mrp_order_form.product_id = self.product_7_3
mrp_order = mrp_order_form.save()
self.assertEqual(mrp_order.bom_id, test_bom)
self.assertEqual(len(mrp_order.workorder_ids), 1)
self.assertEqual(mrp_order.workorder_ids.operation_id, test_bom.operation_ids[0])
self.assertEqual(len(mrp_order.move_byproduct_ids), 1)
self.assertEqual(mrp_order.move_byproduct_ids.product_id, self.product_1)
mrp_order_form = Form(self.env['mrp.production'])
mrp_order_form.product_id = self.product_7_1
mrp_order_form.product_id = self.env['product.product'] # Check form
mrp_order_form.product_id = self.product_7_1
mrp_order_form.bom_id = self.env['mrp.bom'] # Check form
mrp_order_form.bom_id = test_bom
mrp_order = mrp_order_form.save()
self.assertEqual(mrp_order.bom_id, test_bom)
self.assertEqual(len(mrp_order.workorder_ids), 2)
self.assertEqual(mrp_order.workorder_ids.operation_id, test_bom.operation_ids[:2])
self.assertEqual(len(mrp_order.move_byproduct_ids), 2)
self.assertEqual(mrp_order.move_byproduct_ids.product_id, self.product_1 | self.product_2)
mrp_order_form = Form(self.env['mrp.production'])
mrp_order_form.product_id = self.product_7_2
mrp_order = mrp_order_form.save()
self.assertEqual(mrp_order.bom_id, test_bom)
self.assertEqual(len(mrp_order.workorder_ids), 2)
self.assertEqual(mrp_order.workorder_ids.operation_id, test_bom.operation_ids[0] | test_bom.operation_ids[2])
self.assertEqual(len(mrp_order.move_byproduct_ids), 2)
self.assertEqual(mrp_order.move_byproduct_ids.product_id, self.product_1 | self.product_3)
def test_11_multi_level_variants(self):
tmp_picking_type = self.env['stock.picking.type'].create({
'name': 'Manufacturing',
+5 -1
View File
@@ -24,6 +24,8 @@
<field name="product_uom_id" groups="uom.group_uom"/>
</div>
<field name="operation_id" groups="mrp.group_mrp_routings" options="{'no_quick_create':True,'no_create_edit':True}"/>
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="bom_product_template_attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" groups="product.group_product_variant"/>
</group>
</form>
</field>
@@ -89,8 +91,8 @@
<field name="product_qty"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="parent_product_tmpl_id" invisible="1" />
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="bom_product_template_attribute_value_ids" optional="hide" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="operation_id" groups="mrp.group_mrp_routings" optional="hidden" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" options="{'no_quick_create':True,'no_create_edit':True}"/>
@@ -119,6 +121,8 @@
<field name="product_uom_id" groups="uom.group_uom"/>
<field name="allowed_operation_ids" invisible="1"/>
<field name="operation_id" groups="mrp.group_mrp_routings" options="{'no_quick_create':True,'no_create_edit':True}"/>
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="bom_product_template_attribute_value_ids" optional="hide" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
</tree>
</field>
</page>
+7
View File
@@ -14,6 +14,8 @@
<field name="time_computed_on" optional="hide"/>
<field name="time_cycle" widget="float_time" string="Duration (minutes)" width="1.5"/>
<field name="company_id" optional="show" groups="base.group_multi_company"/>
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="bom_product_template_attribute_value_ids" optional="hide" widget="many2many_tags" options="{'no_create': True}" groups="product.group_product_variant"/>
</tree>
</field>
</record>
@@ -32,6 +34,9 @@
<xpath expr="//field[@name='time_cycle']" position="attributes">
<attribute name="sum">Total Duration</attribute>
</xpath>
<xpath expr="//field[@name='bom_product_template_attribute_value_ids']" position="attributes">
<attribute name="attrs">{'column_invisible': [('parent.product_id', '!=', False)]}</attribute>
</xpath>
</field>
</record>
@@ -65,6 +70,8 @@
<field name="workcenter_id" context="{'default_company_id': company_id}"/>
<field name="sequence" groups="base.group_no_one"/>
<field name="bom_id" invisible="context.get('bom_id_invisible', False)"/>
<field name="possible_bom_product_template_attribute_value_ids" invisible="1"/>
<field name="bom_product_template_attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" groups="product.group_product_variant"/>
</group><group name="workorder">
<field name="workorder_count" invisible="1"/>
<field name="time_mode" widget="radio"/>
+4
View File
@@ -69,11 +69,15 @@ class ProductProduct(models.Model):
boms_to_recompute = []
total = 0
for opt in bom.operation_ids:
if opt._skip_operation_line(self):
continue
duration_expected = (
opt.workcenter_id.time_start +
opt.workcenter_id.time_stop +
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