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