[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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user