[IMP] mrp: generate BoM from a MO

In a Manufacturing Order, if the user manufactures a product without any
BoM, they can generate a new BoM who will take the information from the
MO. So, the new BoM will have a BoM line for each MO's component, an
operation for each MO's work order, and the By-Product lines will be
copied too.

task-3089368

Part-of: odoo/odoo#110227
This commit is contained in:
svs-odoo
2023-06-19 16:39:33 +02:00
parent a4c80ebd82
commit e831903612
8 changed files with 395 additions and 14 deletions
+18 -4
View File
@@ -3,8 +3,9 @@
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError, ValidationError
from odoo.osv.expression import AND, NEGATIVE_TERM_OPERATORS
from odoo.osv.expression import AND
from odoo.tools import float_round
from odoo.tools.misc import clean_context
from collections import defaultdict
@@ -168,7 +169,10 @@ class MrpBom(models.Model):
@api.onchange('product_tmpl_id')
def onchange_product_tmpl_id(self):
if self.product_tmpl_id:
self.product_uom_id = self.product_tmpl_id.uom_id.id
default_uom_id = self.env.context.get('default_product_uom_id')
# Avoids updating the BoM's UoM in case a specific UoM was passed through as a default value.
if self.product_uom_id.category_id != self.product_tmpl_id.uom_id.category_id or self.product_uom_id.id != default_uom_id:
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
self.bom_line_ids.bom_product_template_attribute_value_ids = False
@@ -181,8 +185,18 @@ class MrpBom(models.Model):
number_of_bom_of_this_product = self.env['mrp.bom'].search_count(domain)
if number_of_bom_of_this_product: # add a reference to the bom if there is already a bom for this product
self.code = _("%s (new) %s", self.product_tmpl_id.name, number_of_bom_of_this_product)
else:
self.code = False
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
# Checks if the BoM was created from a Manufacturing Order (through Generate BoM action).
parent_production_id = self.env.context.get('parent_production_id')
if parent_production_id: # In this case, assign the newly created BoM to the MO.
# Clean context to avoid parasitic default values.
self.env.context = clean_context(self.env.context)
production = self.env['mrp.production'].browse(parent_production_id)
production._link_bom(res[0])
return res
def copy(self, default=None):
res = super().copy(default)
+159
View File
@@ -897,6 +897,31 @@ class MrpProduction(models.Model):
copied_workorder.blocked_by_workorder_ids = dependencies
return res
def action_generate_bom(self):
""" Generates a new Bill of Material based on the Manufacturing Order's product, components,
workorders and by-products, and assigns it to the MO. Returns a new BoM's form view action.
"""
self.ensure_one()
action = self.env['ir.actions.act_window']._for_xml_id('mrp.mrp_bom_form_action')
action['view_mode'] = 'form'
action['views'] = [(False, 'form')]
action['target'] = 'new'
bom_lines_vals, byproduct_vals, operations_vals = self._get_bom_values()
action['context'] = {
'default_bom_line_ids': bom_lines_vals,
'default_byproduct_ids': byproduct_vals,
'default_code': _("New BoM from %(mo_name)s", mo_name=self.display_name),
'default_company_id': self.company_id.id,
'default_operation_ids': operations_vals,
'default_product_id': self.product_id.id,
'default_product_qty': self.product_qty,
'default_product_tmpl_id': self.product_id.product_tmpl_id.id,
'default_product_uom_id': self.product_uom_id.id,
'parent_production_id': self.id, # Used to assign the new BoM to the current MO.
}
return action
def action_view_mo_delivery(self):
""" Returns an action that display picking related to manufacturing order.
It can either be a list view or in a form view (if there is only one picking to show).
@@ -931,6 +956,30 @@ class MrpProduction(models.Model):
action['context']['warehouse'] = warehouse.id
return action
def _get_bom_values(self, ratio=1):
""" Returns the BoM lines, by-products and operations values needed to
create a new BoM from this Manufacturing Order.
:return: A tuple containing the BoM lines, by-products and operations values, in this order
:rtype: tuple(dict, dict, dict)
"""
self.ensure_one()
# BoM lines values.
bom_lines_values = [Command.create({
'product_id': move_raw.product_id.id,
'product_qty': (move_raw.quantity_done or move_raw.product_uom_qty) * ratio,
'product_uom_id': move_raw.product_uom.id,
}) for move_raw in self.move_raw_ids]
# By-Product lines values.
byproduct_values = [Command.create({
'cost_share': move_byproduct.cost_share,
'product_id': move_byproduct.product_id.id,
'product_qty': (move_byproduct.quantity_done or move_byproduct.product_uom_qty) * ratio,
'product_uom_id': move_byproduct.product_uom.id,
}) for move_byproduct in self.move_byproduct_ids]
# Operations values.
operations_values = [Command.create(wo._get_operation_values()) for wo in self.workorder_ids]
return (bom_lines_values, byproduct_values, operations_values)
@api.model
def _get_default_picking_type_id(self, company_id):
return self.env['stock.picking.type'].search([
@@ -2093,6 +2142,109 @@ class MrpProduction(models.Model):
def _has_workorders(self):
return self.workorder_ids
def _link_bom(self, bom):
""" Links the given BoM to the MO. Assigns BoM's lines, by-products and operations
to the corresponding MO's components, by-products and workorders.
This method assumes the production isn't linked to any BoM yet.
"""
self.ensure_one()
moves_to_unlink = self.env['stock.move']
workorders_to_unlink = self.env['mrp.workorder']
# For draft MO, all the work will be done by compute methods.
# For cancelled and done MO, we don't want to do anything more than assinging the BoM.
if self.state == 'draft' and self.bom_id == bom:
# Empties `bom_id` field so when the BoM is reassigns to this field, depending computes
# will be triggered (doesn't happen if the field's value doesn't change).
self.bom_id = False
if self.state in ['cancel', 'done', 'draft']:
if self.state == 'draft':
# Don't straight delete the moves/workorders to avoid to cancel the MO, those will
# be deleted once the BoM is assigned (and thus after new moves/WO were created).
moves_to_unlink = self.move_raw_ids
workorders_to_unlink = self.workorder_ids
self.bom_id = bom
moves_to_unlink.unlink()
workorders_to_unlink.unlink()
return
def operation_key_values(record):
return tuple(record[key] for key in ('company_id', 'name', 'workcenter_id'))
def filter_by_attributes(record):
product_attribute_ids = self.product_id.product_template_attribute_value_ids.ids
return not record.bom_product_template_attribute_value_ids or\
any(att_val.id in product_attribute_ids for att_val in record.bom_product_template_attribute_value_ids)
ratio = self._get_ratio_between_mo_and_bom_quantities(bom)
bom_lines = list(bom.bom_line_ids.filtered(filter_by_attributes))
bom_byproducts = list(bom.byproduct_ids.filtered(filter_by_attributes))
operations = list(bom.operation_ids.filtered(filter_by_attributes))
# Compares the BoM's lines to the MO's components.
for move_raw in self.move_raw_ids:
for i, bom_line in enumerate(bom_lines):
if bom_line.product_id == move_raw.product_id:
move_raw.bom_line_id = bom_line
move_raw.product_uom_qty = bom_line.product_qty / ratio
move_raw.product_uom = bom_line.product_uom_id
del bom_lines[i]
break
# Creates a raw moves for each remaining BoM's lines.
raw_moves_values = []
for bom_line in bom_lines:
raw_move_vals = self._get_move_raw_values(
bom_line.product_id,
bom_line.product_qty / ratio,
bom_line.product_uom_id,
bom_line=bom_line
)
raw_moves_values.append(raw_move_vals)
self.move_raw_ids += self.env['stock.move'].create(raw_moves_values)
# Compares the BoM's and the MO's by-products.
for move_byproduct in self.move_byproduct_ids:
for i, bom_byproduct in enumerate(bom_byproducts):
if bom_byproduct.product_id == move_byproduct.product_id:
move_byproduct.byproduct_id = bom_byproduct
move_byproduct.cost_share = bom_byproduct.cost_share
move_byproduct.product_uom_qty = bom_byproduct.product_qty / ratio
move_byproduct.product_uom = bom_byproduct.product_uom_id
del bom_byproducts[i]
break
# For each remaining BoM's by-product, creates a move finished.
byproduct_values = []
for bom_byproduct in bom_byproducts:
qty = bom_byproduct.product_qty / ratio
move_byproduct_vals = self._get_move_finished_values(
bom_byproduct.product_id.id, qty, bom_byproduct.product_uom_id.id,
bom_byproduct.operation_id.id, bom_byproduct.id, bom_byproduct.cost_share
)
byproduct_values.append(move_byproduct_vals)
self.move_finished_ids += self.env['stock.move'].create(byproduct_values)
# Compares the BoM's operations to the MO's workoders.
for workorder in self.workorder_ids:
for i, operation in enumerate(operations):
if operation_key_values(operation) == operation_key_values(workorder):
workorder.operation_id = operation
del operations[i]
break
# Creates a workorder for each remaining operation.
workorders_values = []
for operation in operations:
workorder_vals = {
'name': operation.name,
'operation_id': operation.id,
'product_uom_id': self.product_uom_id.id,
'production_id': self.id,
'state': 'pending',
'workcenter_id': operation.workcenter_id.id,
}
workorders_values.append(workorder_vals)
self.workorder_ids += self.env['mrp.workorder'].create(workorders_values)
self.bom_id = bom
@api.model
def _prepare_procurement_group_vals(self, values):
return {'name': values['name']}
@@ -2101,6 +2253,13 @@ class MrpProduction(models.Model):
self.ensure_one()
return max(self.product_qty - self.qty_producing, 0)
def _get_ratio_between_mo_and_bom_quantities(self, bom):
self.ensure_one()
bom_product_uom = (bom.product_id or bom.product_tmpl_id).uom_id
bom_qty = bom.product_uom_id._compute_quantity(bom.product_qty, bom_product_uom)
ratio = bom_qty / self.product_uom_qty
return ratio
def _check_sn_uniqueness(self):
""" Alert the user if the serial number as already been consumed/produced """
if self.product_tracking == 'serial' and self.lot_producing_id:
+12
View File
@@ -784,6 +784,18 @@ class MrpWorkorder(models.Model):
res[wo1].append(wo2)
return res
def _get_operation_values(self):
self.ensure_one()
ratio = 1 / self.qty_production
if self.operation_id.bom_id:
ratio = self.production_id._get_ratio_between_mo_and_bom_quantities(self.operation_id.bom_id)
return {
'company_id': self.company_id.id,
'name': self.name,
'time_cycle_manual': self.duration_expected * ratio,
'workcenter_id': self.workcenter_id.id,
}
def _prepare_timeline_vals(self, duration, date_start, date_end=False):
# Need a loss in case of the real time exceeding the expected
if not self.duration_expected or duration <= self.duration_expected:
+190 -2
View File
@@ -693,7 +693,6 @@ class TestBoM(TestMrpCommon):
# 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
self.assertEqual(report_values['lines']['operations_time'], 660.0, 'Operation time should be the same for 1 unit or for the batch')
def test_21_bom_report_variant(self):
""" Test a sub BoM process with multiple variants.
BOM 1:
@@ -1069,7 +1068,7 @@ class TestBoM(TestMrpCommon):
line.product_qty = 5
with self.assertRaises(exceptions.ValidationError), self.cr.savepoint():
bom_finished = bom_finished.save()
def test_validate_bom_line_with_different_product_variant(self):
"""
Can set a BOM line on a BOM with a different product variant as the BOM itself (same product)
@@ -1143,6 +1142,195 @@ class TestBoM(TestMrpCommon):
self.assertEqual(orderpoint.qty_multiple, 2000.0)
self.assertEqual(orderpoint.qty_to_order, 4000.0)
def test_bom_generated_from_mo(self):
""" Creates a Manufacturing Order without BoM, then uses it to generate a new BoM.
Checks the generated BoM has the expected BoM lines, by-products and operations.
"""
# Creates some products.
common_vals = {'type': "product"}
finished_product = self.env['product.product'].create(dict(common_vals, name="Monster in Jar"))
component_1 = self.env['product.product'].create(dict(common_vals, name="Monster"))
component_2 = self.env['product.product'].create(dict(common_vals, name="Jar"))
by_product = self.env['product.product'].create(dict(common_vals, name="Monster's Tears"))
def create_mo(qty_to_produce=1):
# Creates a MO using some components.
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.product_qty = qty_to_produce
for component in [component_1, component_2]:
with mo_form.move_raw_ids.new() as raw_move:
raw_move.product_id = component
raw_move.product_uom_qty = qty_to_produce
return mo_form.save()
def generate_bom_from_mo(mo):
action_generate_new_bom = mo.action_generate_bom()
return Form(self.env['mrp.bom'].with_context(action_generate_new_bom['context'])).save()
# Generates a BoM from a draft MO.
mo_1 = create_mo()
bom_from_mo_1 = generate_bom_from_mo(mo_1)
# Checks the MO's data.
self.assertEqual(mo_1.bom_id, bom_from_mo_1, "New BoM was assigned to the draft MO")
self.assertEqual(len(mo_1.move_raw_ids), 2)
self.assertEqual(mo_1.move_raw_ids[0].bom_line_id, bom_from_mo_1.bom_line_ids[0])
self.assertEqual(mo_1.move_raw_ids[1].bom_line_id, bom_from_mo_1.bom_line_ids[1])
# Checks the BoM's data.
self.assertEqual(bom_from_mo_1.product_id, finished_product)
self.assertRecordValues(bom_from_mo_1.bom_line_ids, [
{'product_id': component_1.id, 'product_qty': 1},
{'product_id': component_2.id, 'product_qty': 1},
])
bom_from_mo_1.active = False # Archives the created BoM to avoid to use it for the next MOs
# Generates a BoM from a done MO.
mo_2 = create_mo()
mo_2.action_confirm()
mo_2.button_mark_done()
move_ids = mo_2.move_raw_ids.ids
bom_from_mo_2 = generate_bom_from_mo(mo_2)
bom_lines = bom_from_mo_2.bom_line_ids
# Checks the MO's data.
self.assertEqual(mo_2.bom_id, bom_from_mo_2, "New BoM should be assigned to the done MO")
# The move lines weren't linked to the BoM lines since the MO is done.
self.assertRecordValues(mo_2.move_raw_ids, [
{'bom_line_id': False, 'id': move_ids[0], 'product_id': bom_lines[0].product_id.id},
{'bom_line_id': False, 'id': move_ids[1], 'product_id': bom_lines[1].product_id.id},
])
# Checks the BoM's data.
self.assertEqual(bom_from_mo_2.product_id, finished_product)
self.assertRecordValues(bom_from_mo_2.bom_line_ids, [
{'product_id': component_1.id, 'product_qty': 1},
{'product_id': component_2.id, 'product_qty': 1},
])
bom_from_mo_2.active = False # Archives the created BoM to avoid to use it for the next MOs
# Generates a BoM from a confirmed MO using operations and by-products.
self.env.user.groups_id += self.env.ref('mrp.group_mrp_byproducts') # Enables by-products.
self.env.user.groups_id += self.env.ref('mrp.group_mrp_routings') # Enables workorders.
# Produces 3 qties to check if the operations' duration will be correctly divided by 3.
mo_3 = create_mo(3)
mo_3.action_confirm()
mo_form = Form(mo_3)
with mo_form.workorder_ids.new() as operation:
operation.name = "Kiss the Monster on its Forehead 😘"
operation.workcenter_id = self.workcenter_1
operation.duration_expected = 60
with mo_form.workorder_ids.new() as operation:
operation.name = "Put the Monster in the Jar!"
operation.workcenter_id = self.workcenter_2
operation.duration_expected = 480
with mo_form.move_byproduct_ids.new() as by_product_line:
by_product_line.product_id = by_product
by_product_line.quantity_done = 6
mo_3 = mo_form.save()
bom_from_mo_3 = generate_bom_from_mo(mo_3)
# Checks the MO's data.
self.assertEqual(mo_3.bom_id, bom_from_mo_3, "New BoM was assigned to the confirmed MO")
self.assertEqual(mo_3.move_raw_ids[0].bom_line_id, bom_from_mo_3.bom_line_ids[0])
self.assertEqual(mo_3.move_raw_ids[1].bom_line_id, bom_from_mo_3.bom_line_ids[1])
self.assertEqual(mo_3.workorder_ids[0].operation_id, bom_from_mo_3.operation_ids[0])
self.assertEqual(mo_3.workorder_ids[1].operation_id, bom_from_mo_3.operation_ids[1])
self.assertEqual(mo_3.move_byproduct_ids.byproduct_id, bom_from_mo_3.byproduct_ids)
# Checks the BoM's data.
self.assertEqual(mo_3.bom_id, bom_from_mo_3)
self.assertEqual(bom_from_mo_3.product_id, finished_product)
self.assertEqual(bom_from_mo_3.product_qty, 3)
self.assertRecordValues(bom_from_mo_3.bom_line_ids, [
{'product_id': component_1.id, 'product_qty': 3},
{'product_id': component_2.id, 'product_qty': 3},
])
# Durations should be divided by 3 (op1: 60 / 3 = 20; op2: 480 / 3 : 160).
self.assertRecordValues(bom_from_mo_3.operation_ids, [
{'name': "Kiss the Monster on its Forehead 😘", 'workcenter_id': self.workcenter_1.id, 'time_cycle': 20},
{'name': "Put the Monster in the Jar!", 'workcenter_id': self.workcenter_2.id, 'time_cycle': 160},
])
self.assertEqual(bom_from_mo_3.byproduct_ids.product_id, by_product)
self.assertEqual(bom_from_mo_3.byproduct_ids.product_qty, 6)
def test_bom_generated_from_mo_with_different_uom(self):
""" Creates a Manufacturing Order without BoM and using different UoM for its raw moves,
then to generate a new BoM from this MO.
Checks the generated BoM has the expected BoM lines UoM and quantity.
"""
self.env.user.groups_id += self.env.ref('uom.group_uom')
uom_unit = self.env.ref('uom.product_uom_unit')
uom_dozen = self.env.ref('uom.product_uom_dozen')
# Creates some products.
common_vals = {'type': "product"}
finished_product = self.env['product.product'].create(dict(common_vals, name="CO² Molecule"))
component_1 = self.env['product.product'].create(dict(common_vals, name="Carbon Molecule"))
component_2 = self.env['product.product'].create(dict(common_vals, name="Oxygen Molecule"))
# Creates a MO.
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.product_qty = 1
mo_form.product_uom_id = uom_dozen
with mo_form.move_raw_ids.new() as raw_move:
raw_move.product_id = component_1
raw_move.product_uom_qty = 12
raw_move.product_uom = uom_unit
with mo_form.move_raw_ids.new() as raw_move:
raw_move.product_id = component_2
raw_move.product_uom_qty = 2
raw_move.product_uom = uom_dozen
mo = mo_form.save()
mo.action_confirm()
# Generates a BoM from the MO and checks its values.
action_generate_new_bom = mo.action_generate_bom()
bom_form = Form(self.env['mrp.bom'].with_context(action_generate_new_bom['context']))
bom_from_mo = bom_form.save()
self.assertEqual(bom_from_mo.product_uom_id, uom_dozen)
self.assertEqual(bom_from_mo.product_qty, 1)
self.assertRecordValues(bom_from_mo.bom_line_ids, [
{'product_id': component_1.id, 'product_qty': 12, 'product_uom_id': uom_unit.id},
{'product_id': component_2.id, 'product_qty': 2, 'product_uom_id': uom_dozen.id},
])
def test_bom_generated_from_mo_with_byproducts(self):
""" Creates a Manufacturing Order without BoM then to generate a new BoM from this MO and
modifies by-products values.
"""
self.env.user.groups_id += self.env.ref('mrp.group_mrp_byproducts') # Enables by-products.
# Creates some products.
common_vals = {'type': "product"}
finished_product = self.env['product.product'].create(dict(common_vals, name="Banana Bread"))
component_1 = self.env['product.product'].create(dict(common_vals, name="Banana"))
component_2 = self.env['product.product'].create(dict(common_vals, name="Sugar, Spice and Everything Nice"))
by_product = self.env['product.product'].create(dict(common_vals, name="Banana Peels"))
# Creates a MO.
mo_form = Form(self.env['mrp.production'])
mo_form.product_id = finished_product
mo_form.product_qty = 1
with mo_form.move_raw_ids.new() as raw_move:
raw_move.product_id = component_1
raw_move.product_uom_qty = 1
with mo_form.move_raw_ids.new() as raw_move:
raw_move.product_id = component_2
raw_move.product_uom_qty = 1
with mo_form.move_byproduct_ids.new() as by_product_move:
by_product_move.product_id = by_product
by_product_move.product_uom_qty = 1
by_product_move.cost_share = 50
mo = mo_form.save()
mo.action_confirm()
self.assertEqual(mo.move_byproduct_ids.cost_share, 50)
# Generates a BoM from the MO and checks its values.
action_generate_new_bom = mo.action_generate_bom()
bom_form = Form(self.env['mrp.bom'].with_context(action_generate_new_bom['context']))
with bom_form.bom_line_ids.edit(0) as bom_line:
bom_line.product_qty = 3
with bom_form.byproduct_ids.edit(0) as bom_by_product:
bom_by_product.product_qty = 3
bom_by_product.cost_share = 10
bom_from_mo = bom_form.save()
self.assertEqual(mo.bom_id, bom_from_mo)
self.assertEqual(mo.move_raw_ids[0].product_uom_qty, 3)
self.assertEqual(mo.move_raw_ids[1].product_uom_qty, 1)
self.assertEqual(mo.move_byproduct_ids.cost_share, 10)
self.assertEqual(mo.move_byproduct_ids.product_uom_qty, 3)
def test_bom_kit_with_sub_kit(self):
p1, p2, p3, p4 = self.make_prods(4)
self.make_bom(p1, p2, p3)
+5 -5
View File
@@ -38,7 +38,7 @@
<field name="arch" type="xml">
<form string="Bill of Material">
<sheet>
<div class="oe_button_box" name="button_box">
<div class="oe_button_box" name="button_box" invisible="context.get('parent_production_id')">
<button name="%(action_mrp_routing_time)d" type="action" class="oe_stat_button" icon="fa-clock-o" groups="mrp.group_mrp_routings">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Operations<br/>Performance</span>
@@ -69,7 +69,7 @@
</group>
<group>
<field name="code"/>
<field name="type" widget="radio"/>
<field name="type" widget="radio" invisible="context.get('parent_production_id')"/>
<p colspan="2" class="oe_grey oe_edit_only" attrs="{'invisible': [('type','!=','phantom')]}">
<ul>
A BoM of type Kit is not produced with a manufacturing order.<br/>
@@ -82,7 +82,7 @@
</li>
</ul>
</p>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True, 'no_open': True}"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True, 'no_open': True}" invisible="context.get('parent_production_id')"/>
</group>
</group>
<notebook>
@@ -93,8 +93,8 @@
<field name="sequence" widget="handle"/>
<field name="product_id" context="{'default_detailed_type': 'product'}"/>
<field name="product_tmpl_id" invisible="1"/>
<button name="action_see_attachments" type="object" icon="fa-files-o" aria-label="Product Attachments" title="Product Attachments" class="float-end"/>
<field name="attachments_count" class="text-start" string=" "/>
<button name="action_see_attachments" type="object" icon="fa-files-o" aria-label="Product Attachments" title="Product Attachments" class="float-end" invisible="context.get('parent_production_id')"/>
<field name="attachments_count" class="text-start" string=" " invisible="context.get('parent_production_id')"/>
<field name="product_qty"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="parent_product_tmpl_id" invisible="1" />
+7 -1
View File
@@ -242,7 +242,13 @@
<label for="bom_id" name="bom_label"/>
<div class='o_row' name="bom_div">
<field name="bom_id"
context="{'default_product_tmpl_id': product_tmpl_id}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
context="{'default_product_tmpl_id': product_tmpl_id}"
attrs="{'readonly': [('state', '!=', 'draft')], 'invisible': [('state', '!=', 'draft'), ('bom_id', '=', False)]}"/>
<button name="action_generate_bom" type="object" icon="fa-plus"
title="Generate a new BoM from this Manufacturing Order" groups="mrp.group_mrp_manager"
attrs="{'invisible': ['|', '|', ('bom_id', '!=', False), ('product_id', '=', False), ('move_raw_ids', '=', [])]}">
<span class="ms-2">Generate BOM</span>
</button>
</div>
<label for="lot_producing_id" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}"/>
<div class="o_row" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('product_tracking', 'in', ('none', False))]}">
+2 -1
View File
@@ -7,6 +7,7 @@
<field name="model">mrp.routing.workcenter</field>
<field name="arch" type="xml">
<tree string="Routing Work Centers" multi_edit="1">
<field name="active" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="name"/>
<field name="bom_id"/>
@@ -49,7 +50,7 @@
<attribute name="attrs">{'column_invisible': [('parent.product_id', '!=', False)]}</attribute>
</xpath>
<xpath expr="//field[@name='bom_product_template_attribute_value_ids']" position="after">
<button name="action_archive" class="oe_right" type="object" string="Archive Operation"/>
<button name="action_archive" class="oe_right" type="object" string="Archive Operation" invisible="context.get('parent_production_id')"/>
</xpath>
<xpath expr="//field[@name='blocked_by_operation_ids']" position="replace">
</xpath>
@@ -12,6 +12,7 @@
</xpath>
<xpath expr="//div[@name='button_box']" position="replace"/>
<xpath expr="//div[hasclass('oe_title')]" position="replace"/>
<xpath expr="//button[@name='action_generate_bom']" position="replace"/>
<xpath expr="//group[@name='group_extra_info']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
@@ -34,7 +35,7 @@
<xpath expr="//field[@name='workorder_ids']" position="replace"/>
<xpath expr="//button[@name='action_generate_serial']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</xpath>
<xpath expr="//field[@name='move_finished_ids']" position="replace"/>
<xpath expr="//field[@name='move_raw_ids']" position="replace">
<field name="bom_product_ids" invisible="1"/>