diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index a47848b2a1d..1d1a580330d 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -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) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index ecd196cf532..158bff19ee2 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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 diff --git a/addons/mrp/models/mrp_routing.py b/addons/mrp/models/mrp_routing.py index 8d597def7aa..d7188958af2 100644 --- a/addons/mrp/models/mrp_routing.py +++ b/addons/mrp/models/mrp_routing.py @@ -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) diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py index a437d323b1a..63b9bf73933 100644 --- a/addons/mrp/models/mrp_unbuild.py +++ b/addons/mrp/models/mrp_unbuild.py @@ -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 diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index 5e0177c4b2b..cfa68561d78 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -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 diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index a726e342751..b2f39ea0e35 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -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: diff --git a/addons/mrp/report/mrp_report_bom_structure.xml b/addons/mrp/report/mrp_report_bom_structure.xml index 2896392430f..30a8547cdf9 100644 --- a/addons/mrp/report/mrp_report_bom_structure.xml +++ b/addons/mrp/report/mrp_report_bom_structure.xml @@ -126,7 +126,7 @@ - + diff --git a/addons/mrp/static/src/js/mrp_bom_report.js b/addons/mrp/static/src/js/mrp_bom_report.js index cb56c7974f0..9bcbcfa2ca6 100644 --- a/addons/mrp/static/src/js/mrp_bom_report.js +++ b/addons/mrp/static/src/js/mrp_bom_report.js @@ -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 diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py index a9c41a1151d..32bc92fd68d 100644 --- a/addons/mrp/tests/test_bom.py +++ b/addons/mrp/tests/test_bom.py @@ -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', diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 9284501c392..5e1b0c71629 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -24,6 +24,8 @@ + + @@ -89,8 +91,8 @@ - + @@ -119,6 +121,8 @@ + + diff --git a/addons/mrp/views/mrp_routing_views.xml b/addons/mrp/views/mrp_routing_views.xml index 2e36cd8a9ef..da9e9cd506a 100644 --- a/addons/mrp/views/mrp_routing_views.xml +++ b/addons/mrp/views/mrp_routing_views.xml @@ -14,6 +14,8 @@ + + @@ -32,6 +34,9 @@ Total Duration + + {'column_invisible': [('parent.product_id', '!=', False)]} + @@ -65,6 +70,8 @@ + + diff --git a/addons/mrp_account/models/product.py b/addons/mrp_account/models/product.py index 703bc163870..3710520247b 100644 --- a/addons/mrp_account/models/product.py +++ b/addons/mrp_account/models/product.py @@ -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