[REF] mrp: review consumption allowing
Now, by default, a Bill of Material have a flexible consumption instead of strict consumption. Also add new consumption choice: a flexible consumption but with a warning when the bom isn't respected. Also, now, the strict (a new warning option) consumption is checked only when we try to mark as done the MO. task-2241471
This commit is contained in:
committed by
Simon Lejeune
parent
0e2765f5fd
commit
5011ec520c
@@ -19,6 +19,7 @@
|
||||
'wizard/mrp_workcenter_block_view.xml',
|
||||
'wizard/stock_warn_insufficient_qty_views.xml',
|
||||
'wizard/mrp_production_backorder.xml',
|
||||
'wizard/mrp_consumption_warning_views.xml',
|
||||
'views/mrp_views_menus.xml',
|
||||
'views/stock_move_views.xml',
|
||||
'views/mrp_workorder_views.xml',
|
||||
|
||||
@@ -698,7 +698,7 @@
|
||||
<value eval="{'quantity_done': 5}"/>
|
||||
</function>
|
||||
|
||||
<function model="mrp.production" name="post_inventory">
|
||||
<function model="mrp.production" name="_post_inventory">
|
||||
<value eval="[ref('mrp.mrp_production_laptop_cust')]"/>
|
||||
</function>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class MrpAbstractWorkorder(models.AbstractModel):
|
||||
product_tracking = fields.Selection(related="product_id.tracking")
|
||||
consumption = fields.Selection([
|
||||
('strict', 'Strict'),
|
||||
('warning', 'Warning'),
|
||||
('flexible', 'Flexible')],
|
||||
required=True,
|
||||
)
|
||||
|
||||
@@ -63,11 +63,15 @@ class MrpBom(models.Model):
|
||||
'res.company', 'Company', index=True,
|
||||
default=lambda self: self.env.company)
|
||||
consumption = fields.Selection([
|
||||
('strict', 'Strict'),
|
||||
('flexible', 'Flexible')],
|
||||
help="Defines if you can consume more or less components than the quantity defined on the BoM.",
|
||||
default='flexible',
|
||||
string='Consumption',
|
||||
('flexible', 'Allowed'),
|
||||
('warning', 'Allowed with warning'),
|
||||
('strict', 'Blocked')],
|
||||
help="Defines if you can consume more or less components than the quantity defined on the BoM:\n"
|
||||
" * Allowed: allowed for all manufacturing users.\n"
|
||||
" * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n"
|
||||
" * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.",
|
||||
default='warning',
|
||||
string='Flexible Consumption',
|
||||
required=True
|
||||
)
|
||||
|
||||
|
||||
@@ -198,9 +198,6 @@ class MrpProduction(models.Model):
|
||||
reserve_visible = fields.Boolean(
|
||||
'Allowed to Reserve Production', compute='_compute_unreserve_visible',
|
||||
help='Technical field to check when we can reserve quantities')
|
||||
post_visible = fields.Boolean(
|
||||
'Allowed to Post Inventory', compute='_compute_post_visible',
|
||||
help='Technical field to check when we can post')
|
||||
user_id = fields.Many2one(
|
||||
'res.users', 'Responsible', default=lambda self: self.env.user,
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
@@ -236,11 +233,12 @@ class MrpProduction(models.Model):
|
||||
delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids')
|
||||
confirm_cancel = fields.Boolean(compute='_compute_confirm_cancel')
|
||||
consumption = fields.Selection([
|
||||
('strict', 'Strict'),
|
||||
('flexible', 'Flexible')],
|
||||
('flexible', 'Allowed'),
|
||||
('warning', 'Allowed with warning'),
|
||||
('strict', 'Blocked')],
|
||||
required=True,
|
||||
readonly=True,
|
||||
default='strict',
|
||||
default='flexible',
|
||||
)
|
||||
|
||||
mrp_production_child_count = fields.Integer("Number of generated MO", compute='_compute_mrp_production_child_count')
|
||||
@@ -404,7 +402,10 @@ class MrpProduction(models.Model):
|
||||
for production in self:
|
||||
production.workorder_done_count = count_data.get(production.id, 0)
|
||||
|
||||
@api.depends('move_raw_ids.state', 'move_finished_ids.state', 'workorder_ids', 'workorder_ids.state', 'qty_produced', 'move_raw_ids.quantity_done', 'product_qty')
|
||||
@api.depends(
|
||||
'move_raw_ids.state', 'move_finished_ids.state', 'workorder_ids',
|
||||
'workorder_ids.date_planned_start', 'workorder_ids.date_planned_finished',
|
||||
'workorder_ids.state', 'move_raw_ids.quantity_done', 'product_qty', 'qty_producing')
|
||||
def _compute_state(self):
|
||||
""" Compute the production state. It use the same process than stock
|
||||
picking. It exists 3 extra steps for production:
|
||||
@@ -424,17 +425,16 @@ class MrpProduction(models.Model):
|
||||
elif all(move.state in ['cancel', 'done'] for move in production.move_raw_ids):
|
||||
if (
|
||||
production.bom_id.consumption == 'flexible'
|
||||
and float_compare(production.qty_produced, production.product_qty, precision_rounding=production.product_uom_id.rounding) == -1
|
||||
and float_compare(production.qty_producing, production.product_qty, precision_rounding=production.product_uom_id.rounding) == -1
|
||||
):
|
||||
production.state = 'progress'
|
||||
else:
|
||||
production.state = 'done'
|
||||
elif production.move_finished_ids.filtered(lambda m: m.state not in ('cancel', 'done') and m.product_id.id == production.product_id.id)\
|
||||
and (production.qty_produced >= production.product_qty)\
|
||||
and (not production.bom_id.operation_ids or all(wo_state in ('cancel', 'done') for wo_state in production.workorder_ids.mapped('state'))):
|
||||
elif production.qty_producing >= production.product_qty:
|
||||
production.state = 'to_close'
|
||||
elif production.workorder_ids and any(wo_state in ('progress') for wo_state in production.workorder_ids.mapped('state'))\
|
||||
or production.qty_produced > 0 and production.qty_produced < production.product_qty:
|
||||
elif production.workorder_ids and any(wo_state == 'progress' for wo_state in production.workorder_ids.mapped('state')):
|
||||
production.state = 'progress'
|
||||
elif not float_is_zero(production.qty_producing, precision_rounding=production.product_uom_id.rounding) and production.qty_producing < production.product_qty:
|
||||
production.state = 'progress'
|
||||
elif production.workorder_ids:
|
||||
production.state = 'planned'
|
||||
@@ -464,11 +464,6 @@ class MrpProduction(models.Model):
|
||||
order.unreserve_visible = not any_quantity_done and already_reserved
|
||||
order.reserve_visible = order.state in ('confirmed', 'progress', 'to_close') and any(move.state in ['confirmed', 'partially_available'] for move in order.move_raw_ids.filtered(lambda m: m.product_uom_qty))
|
||||
|
||||
@api.depends('move_finished_ids.quantity_done', 'move_finished_ids.state', 'is_locked')
|
||||
def _compute_post_visible(self):
|
||||
for order in self:
|
||||
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids)
|
||||
|
||||
@api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked')
|
||||
def _get_produced_qty(self):
|
||||
for production in self:
|
||||
@@ -909,9 +904,7 @@ class MrpProduction(models.Model):
|
||||
def action_confirm(self):
|
||||
self._check_company()
|
||||
for production in self:
|
||||
if not production.bom_id:
|
||||
production.consumption = 'flexible'
|
||||
else:
|
||||
if production.bom_id:
|
||||
production.consumption = production.bom_id.consumption
|
||||
if not production.move_raw_ids:
|
||||
raise UserError(_("Add some materials to consume before marking this MO as to do."))
|
||||
@@ -1060,7 +1053,7 @@ class MrpProduction(models.Model):
|
||||
'operation_id': operation.id,
|
||||
'state': len(workorders) == 0 and 'ready' or 'pending',
|
||||
'qty_producing': quantity,
|
||||
'consumption': self.bom_id.consumption,
|
||||
'consumption': self.consumption,
|
||||
})
|
||||
if workorders:
|
||||
workorders[-1].next_work_order_id = workorder.id
|
||||
@@ -1090,22 +1083,60 @@ class MrpProduction(models.Model):
|
||||
workorder._generate_wo_lines()
|
||||
return workorders
|
||||
|
||||
def _check_lots(self):
|
||||
# Check that the components were consumed for lots that we have produced.
|
||||
if self.product_id.tracking != 'none':
|
||||
finished_lots = self.finished_move_line_ids.mapped('lot_id')
|
||||
raw_finished_lots = self.move_raw_ids.mapped('move_line_ids.lot_produced_ids')
|
||||
if (raw_finished_lots - finished_lots):
|
||||
lots_short = raw_finished_lots - finished_lots
|
||||
error_msg = _(
|
||||
'Some components have been consumed for a lot/serial number that has not been produced. '
|
||||
'Unlock the MO and click on the components lines to correct it.\n'
|
||||
'List of the components:\n'
|
||||
)
|
||||
move_lines = self.move_raw_ids.mapped('move_line_ids').filtered(lambda ml: lots_short & ml.lot_produced_ids)
|
||||
for ml in move_lines:
|
||||
error_msg += ml.product_id.display_name + ' (' + ', '.join((lots_short & ml.lot_produced_ids).mapped('name')) + ')\n'
|
||||
raise UserError(error_msg)
|
||||
def _get_consumption_issues(self):
|
||||
"""Compare the quantity consumed of the components, the expected quantity
|
||||
on the BoM and the consumption parameter on the order.
|
||||
|
||||
:return: list of tuples (order_id, product_id, consumed_qty, expected_qty) where the
|
||||
consumption isn't honored. order_id and product_id are recordset of mrp.production
|
||||
and product.product respectively
|
||||
:rtype: list
|
||||
"""
|
||||
issues = []
|
||||
if self.env.context.get('skip_consumption', False):
|
||||
return issues
|
||||
for order in self:
|
||||
if order.consumption == 'flexible' or not order.bom_id or not order.bom_id.bom_line_ids:
|
||||
continue
|
||||
expected_move_values = order._get_moves_raw_values()
|
||||
expected_qty_by_product = defaultdict(float)
|
||||
for move_values in expected_move_values:
|
||||
move_product = self.env['product.product'].browse(move_values['product_id'])
|
||||
move_uom = self.env['uom.uom'].browse(move_values['product_uom'])
|
||||
move_product_qty = move_uom._compute_quantity(move_values['product_uom_qty'], move_product.uom_id)
|
||||
expected_qty_by_product[move_product] += move_product_qty * order.qty_producing / order.product_qty
|
||||
|
||||
done_qty_by_product = defaultdict(float)
|
||||
for move in order.move_raw_ids:
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
if move.product_id not in expected_qty_by_product:
|
||||
issues.append((order, move.product_id, qty_done, 0.0))
|
||||
continue
|
||||
done_qty_by_product[move.product_id] += qty_done
|
||||
|
||||
for product, qty_to_consume in expected_qty_by_product.items():
|
||||
qty_done = done_qty_by_product.get(product, 0.0)
|
||||
if float_compare(qty_to_consume, qty_done, precision_rounding=product.uom_id.rounding) != 0:
|
||||
issues.append((order, product, qty_done, qty_to_consume))
|
||||
|
||||
return issues
|
||||
|
||||
def _action_generate_consumption_wizard(self, consumption_issues):
|
||||
ctx = self.env.context.copy()
|
||||
lines = []
|
||||
for order, product_id, consumed_qty, expected_qty in consumption_issues:
|
||||
lines.append((0, 0, {
|
||||
'mrp_production_id': order.id,
|
||||
'product_id': product_id.id,
|
||||
'consumption': order.consumption,
|
||||
'product_uom_id': product_id.uom_id.id,
|
||||
'product_consumed_qty_uom': consumed_qty,
|
||||
'product_expected_qty_uom': expected_qty
|
||||
}))
|
||||
ctx.update({'default_mrp_production_ids': self.ids, 'default_mrp_consumption_warning_line_ids': lines})
|
||||
action = self.env.ref('mrp.action_mrp_consumption_warning').read()[0]
|
||||
action['context'] = ctx
|
||||
return action
|
||||
|
||||
def _get_quantity_produced_issues(self):
|
||||
quantity_issues = []
|
||||
@@ -1268,8 +1299,7 @@ class MrpProduction(models.Model):
|
||||
def button_mark_done(self):
|
||||
self._check_company()
|
||||
for order in self:
|
||||
# TODO : multi _check_lots and _check_sn_uniqueness + error message with MO name
|
||||
order._check_lots()
|
||||
# TODO : multi _check_sn_uniqueness + error message with MO name
|
||||
order._check_sn_uniqueness()
|
||||
|
||||
res = self._pre_button_mark_done()
|
||||
@@ -1338,6 +1368,10 @@ class MrpProduction(models.Model):
|
||||
return action
|
||||
|
||||
def _pre_button_mark_done(self):
|
||||
consumption_issues = self._get_consumption_issues()
|
||||
if consumption_issues:
|
||||
return self._action_generate_consumption_wizard(consumption_issues)
|
||||
|
||||
quantity_issues = self._get_quantity_produced_issues()
|
||||
if quantity_issues:
|
||||
return self._action_generate_backorder_wizard(quantity_issues)
|
||||
@@ -1455,7 +1489,7 @@ class MrpProduction(models.Model):
|
||||
|
||||
def _get_quantity_to_backorder(self):
|
||||
self.ensure_one()
|
||||
return max(self.product_qty - self.qty_produced, 0)
|
||||
return max(self.product_qty - self.qty_producing, 0)
|
||||
|
||||
def _check_sn_uniqueness(self):
|
||||
""" Alert the user if the serial number as already been consumed/produced """
|
||||
@@ -1530,10 +1564,3 @@ class MrpProduction(models.Model):
|
||||
duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line
|
||||
if duplicates:
|
||||
raise UserError(message)
|
||||
|
||||
def _strict_consumption_check(self):
|
||||
if self.consumption == 'strict':
|
||||
for move in self.move_raw_ids:
|
||||
rounding = move.product_uom.rounding
|
||||
if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=rounding) != 0:
|
||||
raise UserError(_('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.') % move.product_id.name)
|
||||
|
||||
@@ -61,3 +61,5 @@ access_change_production_qty,access.change.production.qty,model_change_productio
|
||||
access_stock_warn_insufficient_qty_unbuild,access.stock.warn.insufficient.qty.unbuild,model_stock_warn_insufficient_qty_unbuild,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_production_backorder,access.mrp.production.backorder,model_mrp_production_backorder,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_production_backorder_line,access.mrp.production.backorder.line,model_mrp_production_backorder_line,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_consumption_warning,access.mrp.consumption.warning,model_mrp_consumption_warning,mrp.group_mrp_user,1,1,1,0
|
||||
access_mrp_consumption_warning_line,access.mrp.consumption.warning.line,model_mrp_consumption_warning_line,mrp.group_mrp_user,1,1,1,0
|
||||
|
||||
|
@@ -100,6 +100,7 @@ class TestMrpCommon(common2.TestStockCommon):
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
],
|
||||
'type': 'normal',
|
||||
@@ -111,6 +112,7 @@ class TestMrpCommon(common2.TestStockCommon):
|
||||
'product_id': cls.product_5.id,
|
||||
'product_tmpl_id': cls.product_5.product_tmpl_id.id,
|
||||
'product_uom_id': cls.product_5.uom_id.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
@@ -125,6 +127,7 @@ class TestMrpCommon(common2.TestStockCommon):
|
||||
'product_id': cls.product_6.id,
|
||||
'product_tmpl_id': cls.product_6.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_dozen.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 2.0,
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Cutting Machine', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 12, 'sequence': 1}),
|
||||
|
||||
@@ -157,6 +157,7 @@ class TestBoM(TestMrpCommon):
|
||||
'product_tmpl_id': self.product_9.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_9.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal'
|
||||
})
|
||||
test_bom_4 = self.env['mrp.bom'].create({
|
||||
@@ -164,6 +165,7 @@ class TestBoM(TestMrpCommon):
|
||||
'product_tmpl_id': self.product_10.product_tmpl_id.id,
|
||||
'product_uom_id': self.product_10.uom_id.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'phantom'
|
||||
})
|
||||
test_bom_3_l1 = self.env['mrp.bom.line'].create({
|
||||
|
||||
@@ -79,7 +79,7 @@ class TestMrpByProduct(common.TransactionCase):
|
||||
self.assertEqual(consume_move_c.product_uom_qty, 4, "Wrong consumed quantity of product c.")
|
||||
self.assertEqual(by_product_move.product_uom_qty, 2, "Wrong produced quantity of sub product.")
|
||||
|
||||
mnf_product_a.post_inventory()
|
||||
mnf_product_a._post_inventory()
|
||||
|
||||
# I see that stock moves of External Hard Disk including Headset USB are done now.
|
||||
self.assertFalse(any(move.state != 'done' for move in moves), 'Moves are not done!')
|
||||
|
||||
@@ -58,7 +58,7 @@ class TestMrpCancelMO(TestMrpCommon):
|
||||
mo_form.qty_producing = 2
|
||||
manufacturing_order = mo_form.save()
|
||||
# Post Inventory
|
||||
manufacturing_order.post_inventory()
|
||||
manufacturing_order._post_inventory()
|
||||
# Cancel the MO
|
||||
manufacturing_order.action_cancel()
|
||||
# Check MO is marked as done and its SML are done or cancelled
|
||||
@@ -108,7 +108,7 @@ class TestMrpCancelMO(TestMrpCommon):
|
||||
workorder._apply_update_workorder_lines()
|
||||
workorder.record_production()
|
||||
# Post Inventory
|
||||
manufacturing_order.post_inventory()
|
||||
manufacturing_order._post_inventory()
|
||||
# Cancel it
|
||||
manufacturing_order.action_cancel()
|
||||
# Check MO is done, WO is cancelled and its SML are done or cancelled
|
||||
@@ -147,7 +147,7 @@ class TestMrpCancelMO(TestMrpCommon):
|
||||
mo_form.qty_producing = 2
|
||||
manufacturing_order = mo_form.save()
|
||||
# Post Inventory
|
||||
manufacturing_order.post_inventory()
|
||||
manufacturing_order._post_inventory()
|
||||
# Unlink the MO must raises an UserError since it cannot be really cancelled
|
||||
self.assertEqual(manufacturing_order.exists().state, 'progress')
|
||||
with self.assertRaises(UserError):
|
||||
|
||||
+136
-114
@@ -101,8 +101,9 @@ class TestMrpOrder(TestMrpCommon):
|
||||
action = man_order.button_mark_done()
|
||||
self.assertEqual(man_order.state, 'progress', "Production order should be open a backorder wizard, then not done yet.")
|
||||
|
||||
quantity_issues = man_order._get_consumption_issues()
|
||||
action = man_order._action_generate_consumption_wizard(quantity_issues)
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
|
||||
backorder.save().action_close_mo()
|
||||
self.assertEqual(man_order.state, 'done', "Production order should be done.")
|
||||
|
||||
@@ -153,14 +154,20 @@ class TestMrpOrder(TestMrpCommon):
|
||||
It should create extra move and share the quantity between the two stock
|
||||
moves """
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(qty_base_1=10, qty_final=1, qty_base_2=1)
|
||||
bom.consumption = 'flexible'
|
||||
mo.action_assign()
|
||||
# check is_quantity_done_editable
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 1
|
||||
for i in range(len(mo_form.move_raw_ids)):
|
||||
with mo_form.move_raw_ids.edit(i) as move:
|
||||
move.quantity_done += 1
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[0], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 2
|
||||
details_operation_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.edit(0) as ml:
|
||||
ml.qty_done = 11
|
||||
details_operation_form.save()
|
||||
|
||||
self.assertEqual(len(mo.move_raw_ids), 2)
|
||||
self.assertEqual(len(mo.move_raw_ids.mapped('move_line_ids')), 2)
|
||||
self.assertEqual(mo.move_raw_ids[0].move_line_ids.mapped('qty_done'), [2])
|
||||
@@ -173,95 +180,6 @@ class TestMrpOrder(TestMrpCommon):
|
||||
self.assertEqual(mo.move_raw_ids.mapped('quantity_done'), [1, 10, 1, 1])
|
||||
self.assertEqual(mo.move_raw_ids.mapped('move_line_ids.qty_done'), [1, 10, 1, 1])
|
||||
|
||||
def test_multiple_post_inventory(self):
|
||||
""" Check the consumed quants of the produced quants when intermediate calls to `post_inventory` during a MO."""
|
||||
|
||||
# create a bom for `custom_laptop` with components that aren't tracked
|
||||
unit = self.ref("uom.product_uom_unit")
|
||||
custom_laptop = self.env['product.product'].create({
|
||||
'name': 'Drawer',
|
||||
'type': 'product',
|
||||
'uom_id': unit,
|
||||
'uom_po_id': unit,
|
||||
})
|
||||
|
||||
product_charger = self.env['product.product'].create({
|
||||
'name': 'Charger',
|
||||
'type': 'product',
|
||||
'uom_id': unit,
|
||||
'uom_po_id': unit})
|
||||
product_keybord = self.env['product.product'].create({
|
||||
'name': 'Usb Keybord',
|
||||
'type': 'product',
|
||||
'uom_id': unit,
|
||||
'uom_po_id': unit})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': custom_laptop.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit,
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': product_charger.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit
|
||||
}), (0, 0, {
|
||||
'product_id': product_keybord.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit
|
||||
})]
|
||||
})
|
||||
|
||||
# put the needed products in stock
|
||||
source_location_id = self.stock_location_14.id
|
||||
quant_before = custom_laptop.qty_available
|
||||
inventory = self.env['stock.inventory'].create({
|
||||
'name': 'Inventory Product Table',
|
||||
'line_ids': [(0, 0, {
|
||||
'product_id': product_charger.id,
|
||||
'product_uom_id': product_charger.uom_id.id,
|
||||
'product_qty': 2,
|
||||
'location_id': source_location_id
|
||||
}), (0, 0, {
|
||||
'product_id': product_keybord.id,
|
||||
'product_uom_id': product_keybord.uom_id.id,
|
||||
'product_qty': 2,
|
||||
'location_id': source_location_id
|
||||
})]
|
||||
})
|
||||
inventory.action_start()
|
||||
inventory.action_validate()
|
||||
|
||||
# create a mo for this bom
|
||||
mo_custom_laptop_form = Form(self.env['mrp.production'])
|
||||
mo_custom_laptop_form.product_id = custom_laptop
|
||||
mo_custom_laptop_form.product_qty = 2
|
||||
mo_custom_laptop = mo_custom_laptop_form.save()
|
||||
mo_custom_laptop.action_confirm()
|
||||
mo_custom_laptop.action_assign()
|
||||
self.assertEqual(mo_custom_laptop.reservation_state, 'assigned')
|
||||
|
||||
# produce one item, call `post_inventory`
|
||||
mo_form = Form(mo_custom_laptop)
|
||||
mo_form.qty_producing = 1.00
|
||||
mo_custom_laptop = mo_form.save()
|
||||
mo_custom_laptop.post_inventory()
|
||||
|
||||
# check the consumed quants of the produced quant
|
||||
first_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'done')
|
||||
quant_after1 = custom_laptop.qty_available
|
||||
self.assertEqual(first_move.quantity_done, 1, "Order already produce 1 product")
|
||||
self.assertEqual(quant_after1 - quant_before, 1, "1 product available after production")
|
||||
second_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'confirmed')
|
||||
self.assertEqual(second_move.quantity_done, 0, "There is still one product to pruduce")
|
||||
|
||||
# produce the second item, call `post_inventory`
|
||||
mo_form = Form(mo_custom_laptop)
|
||||
mo_form.qty_producing = 2.00
|
||||
mo_custom_laptop = mo_form.save()
|
||||
mo_custom_laptop.post_inventory()
|
||||
self.assertEqual(second_move.quantity_done, 1, "Order produce the second product")
|
||||
quant_after2 = custom_laptop.qty_available
|
||||
self.assertEqual(quant_after2 - quant_before, 2, "2 products available after production")
|
||||
|
||||
def test_update_quantity_1(self):
|
||||
""" Build 5 final products with different consumed lots,
|
||||
then edit the finished quantity and update the Manufacturing
|
||||
@@ -470,7 +388,6 @@ class TestMrpOrder(TestMrpCommon):
|
||||
mo, _, p_final, p1, p2 = self.generate_mo(tracking_base_1='lot', qty_base_1=10, qty_final=1)
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
mo.bom_id.consumption = 'flexible' # Because we will over consume.
|
||||
first_lot_for_p1 = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': p1.id,
|
||||
@@ -592,7 +509,7 @@ class TestMrpOrder(TestMrpCommon):
|
||||
mo_form.qty_producing = 3
|
||||
mo = mo_form.save()
|
||||
|
||||
mo.post_inventory()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(len(mo.move_raw_ids), 4)
|
||||
|
||||
mo.move_raw_ids.filtered(lambda m: m.state != 'done')[0].quantity_done = 3
|
||||
@@ -611,11 +528,11 @@ class TestMrpOrder(TestMrpCommon):
|
||||
self.assertTrue(all(s == 'done' for s in mo.move_raw_ids.mapped('state')))
|
||||
self.assertEqual(sum(mo.move_raw_ids.mapped('move_line_ids.product_uom_qty')), 0)
|
||||
|
||||
def test_product_produce_9(self):
|
||||
def test_consumption_strict_1(self):
|
||||
""" Checks the constraints of a strict BOM without tracking when playing around
|
||||
quantities to consume."""
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(consumption='strict')
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(consumption='strict', qty_final=1)
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
@@ -623,22 +540,122 @@ class TestMrpOrder(TestMrpCommon):
|
||||
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo.with_context({'debug': True}))
|
||||
mo_form = Form(mo)
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
# try adding another line for a bom product to increase the quantity
|
||||
mo_form.qty_producing = 1
|
||||
# try adding another line for a bom product to increase the quantity
|
||||
mo_form.qty_producing = 1
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = p1
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
# Won't accept to be done, instead return a wizard
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'to_close')
|
||||
consumption_issues = mo._get_consumption_issues()
|
||||
action = mo._action_generate_consumption_wizard(consumption_issues)
|
||||
warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
|
||||
warning = warning.save()
|
||||
|
||||
self.assertEqual(len(warning.mrp_consumption_warning_line_ids), 1)
|
||||
self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_consumed_qty_uom, 5)
|
||||
self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_expected_qty_uom, 4)
|
||||
# Force the warning (as a manager)
|
||||
warning.action_confirm()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
def test_consumption_warning_1(self):
|
||||
""" Checks the constraints of a strict BOM without tracking when playing around
|
||||
quantities to consume."""
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(consumption='warning', qty_final=1)
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
|
||||
# try adding another line for a bom product to increase the quantity
|
||||
mo_form.qty_producing = 1
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = p1
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
|
||||
# Won't accept to be done, instead return a wizard
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'to_close')
|
||||
|
||||
consumption_issues = mo._get_consumption_issues()
|
||||
action = mo._action_generate_consumption_wizard(consumption_issues)
|
||||
warning = Form(self.env['mrp.consumption.warning'].with_context(**action['context']))
|
||||
warning = warning.save()
|
||||
|
||||
self.assertEqual(len(warning.mrp_consumption_warning_line_ids), 1)
|
||||
self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_consumed_qty_uom, 5)
|
||||
self.assertEqual(warning.mrp_consumption_warning_line_ids[0].product_expected_qty_uom, 4)
|
||||
# Force the warning (as a manager or employee)
|
||||
warning.action_confirm()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
def test_consumption_flexible_1(self):
|
||||
""" Checks the constraints of a strict BOM without tracking when playing around
|
||||
quantities to consume."""
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo(consumption='flexible', qty_final=1)
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo)
|
||||
|
||||
# try adding another line for a bom product to increase the quantity
|
||||
mo_form.qty_producing = 1
|
||||
with mo_form.move_raw_ids.new() as line:
|
||||
line.product_id = p1
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 1
|
||||
details_operation_form.save()
|
||||
|
||||
# Won't accept to be done, instead return a wizard
|
||||
mo.button_mark_done()
|
||||
self.assertEqual(mo.state, 'done')
|
||||
|
||||
def test_product_produce_9(self):
|
||||
""" Checks the production wizard contains lines even for untracked products. """
|
||||
serial = self.env['product.product'].create({
|
||||
'name': 'S1',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo()
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 100)
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
|
||||
mo.action_assign()
|
||||
mo_form = Form(mo)
|
||||
|
||||
# change the quantity done in one line
|
||||
with self.assertRaises(AssertionError):
|
||||
with mo_form.move_raw_ids.new() as move:
|
||||
move.product_id = p1
|
||||
move.quantity_done = 1
|
||||
mo = mo_form.save()
|
||||
mo.button_mark_done()
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
# Try updating qty_done
|
||||
mo = mo_form.save()
|
||||
mo.move_raw_ids[0].move_line_ids[0].qty_done += 1
|
||||
mo.button_mark_done()
|
||||
move.product_id = serial
|
||||
move.quantity_done = 2
|
||||
mo_form.save()
|
||||
|
||||
def test_product_produce_10(self):
|
||||
""" Produce byproduct with serial, lot and not tracked.
|
||||
@@ -817,7 +834,7 @@ class TestMrpOrder(TestMrpCommon):
|
||||
mo.bom_id.consumption = 'flexible' # Because we'll over-consume with a product not defined in the BOM
|
||||
mo.action_assign()
|
||||
|
||||
mo_form = Form(mo.with_context({'debug': True}))
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 3
|
||||
self.assertEqual(sum([x['quantity_done'] for x in mo_form.move_raw_ids._records]), 15, 'Update the produce quantity should change the components quantity.')
|
||||
self.assertEqual(sum([x['reserved_availability'] for x in mo_form.move_raw_ids._records]), 5, 'Update the produce quantity should not change the components reserved quantity.')
|
||||
@@ -830,8 +847,13 @@ class TestMrpOrder(TestMrpCommon):
|
||||
# try adding another product that doesn't belong to the BoM
|
||||
with mo_form.move_raw_ids.new() as move:
|
||||
move.product_id = self.product_4
|
||||
move.quantity_done = 10
|
||||
mo = mo_form.save()
|
||||
details_operation_form = Form(mo.move_raw_ids[-1], view=self.env.ref('stock.view_stock_move_operations'))
|
||||
with details_operation_form.move_line_ids.new() as ml:
|
||||
ml.qty_done = 10
|
||||
details_operation_form.save()
|
||||
# Check that this new product is not updated by qty_producing
|
||||
mo_form = Form(mo)
|
||||
mo_form.qty_producing = 2
|
||||
for move in mo_form.move_raw_ids._records:
|
||||
if move['product_id'] == self.product_4.id:
|
||||
|
||||
@@ -42,6 +42,7 @@ class TestWarehouse(common.TestMrpCommon):
|
||||
'product_tmpl_id': self.laptop.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id,
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': self.graphics_card.id,
|
||||
'product_qty': 1,
|
||||
|
||||
@@ -147,6 +147,7 @@ class TestTraceability(TestMrpCommon):
|
||||
'product_tmpl_id': product_final.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product_1.id, 'product_qty': 1}),
|
||||
|
||||
@@ -36,6 +36,7 @@ class TestWorkOrderProcessCommon(TestMrpCommon):
|
||||
'product_tmpl_id': cls.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 4.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {'name': 'Gift Wrap Maching', 'workcenter_id': cls.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
|
||||
],
|
||||
@@ -377,7 +378,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
# ---------------------------------------------------------------
|
||||
# Check consume quants and produce quants after posting inventory
|
||||
# ---------------------------------------------------------------
|
||||
production_table.post_inventory()
|
||||
production_table._post_inventory()
|
||||
self.assertEqual(sum(move_table_sheet.mapped('quantity_done')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name)
|
||||
self.assertEqual(sum(move_leg.mapped('quantity_done')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name)
|
||||
self.assertEqual(sum(move_table_bolt.mapped('quantity_done')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name)
|
||||
@@ -556,6 +557,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
'product_tmpl_id': custom_laptop.product_tmpl_id.id,
|
||||
'product_qty': 10,
|
||||
'product_uom_id': unit,
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': product_charger.id,
|
||||
'product_qty': 20,
|
||||
@@ -691,6 +693,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
'product_tmpl_id': laptop.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'product_uom_id': unit.id,
|
||||
'consumption': 'flexible',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': graphics_card.id,
|
||||
'product_qty': 1,
|
||||
@@ -773,6 +776,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': drawer.product_tmpl_id.id,
|
||||
'product_uom_id': self.env.ref('uom.product_uom_unit').id,
|
||||
'consumption': 'flexible',
|
||||
'sequence': 2,
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
@@ -1274,6 +1278,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
'product_tmpl_id': self.product_4.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': self.product_2.id, 'product_qty': 1}),
|
||||
@@ -1286,6 +1291,7 @@ class TestWorkOrderProcess(TestWorkOrderProcessCommon):
|
||||
'product_tmpl_id': self.product_5.product_tmpl_id.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'operation_ids': [
|
||||
(0, 0, {
|
||||
'workcenter_id': self.mrp_workcenter_1.id,
|
||||
@@ -1465,6 +1471,7 @@ class TestRoutingAndKits(SavepointCase):
|
||||
'product_id': cls.finished1.id,
|
||||
'product_tmpl_id': cls.finished1.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'consumption': 'flexible',
|
||||
'product_qty': 1,
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
|
||||
@@ -114,8 +114,8 @@
|
||||
<page string="Miscellaneous" name="miscellaneous">
|
||||
<group>
|
||||
<group>
|
||||
<field name="ready_to_produce" attrs="{'invisible': [('type','!=','normal')]}" string="Manufacturing Readiness" groups="mrp.group_mrp_routings"/>
|
||||
<field name="consumption" attrs="{'invisible': [('type','!=','normal')]}"/>
|
||||
<field name="ready_to_produce" attrs="{'invisible': [('type','!=','normal')]}" string="Manufacturing Readiness" widget="radio" groups="mrp.group_mrp_routings"/>
|
||||
<field name="consumption" attrs="{'invisible': [('type','!=','normal')]}" widget="radio"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="picking_type_id" attrs="{'invisible': [('type','!=','normal')]}" string="Operation" groups="stock.group_adv_location"/>
|
||||
|
||||
@@ -39,6 +39,20 @@
|
||||
<field name="code">records.button_plan()</field>
|
||||
</record>
|
||||
|
||||
<record id="action_production_order_mark_done" model="ir.actions.server">
|
||||
<field name="name">Mark as Done</field>
|
||||
<field name="model_id" ref="mrp.model_mrp_production"/>
|
||||
<field name="binding_model_id" ref="mrp.model_mrp_production"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
if records:
|
||||
res = records.filtered(lambda mo: mo.state in {'to_close', 'progress'}).button_mark_done()
|
||||
if res is not True:
|
||||
action = res
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_production_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.production.form</field>
|
||||
<field name="model">mrp.production</field>
|
||||
@@ -53,7 +67,6 @@
|
||||
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object" class="oe_highlight"/>
|
||||
<button name="button_plan" attrs="{'invisible': ['|', '|', ('state', 'not in', ('confirmed', 'progress')), ('workorder_ids', '=', []), ('is_planned', '=', True)]}" type="object" string="Plan" class="oe_highlight"/>
|
||||
<button name="button_unplan" type="object" string="Unplan" attrs="{'invisible': ['|', '|', ('state', '!=', 'planned'), ('date_planned_start', '=', False), ('date_planned_finished', '=', False)]}"/>
|
||||
<button name="post_inventory" string="Post Inventory" type="object" attrs="{'invisible': [('post_visible', '=', False)]}"/>
|
||||
<button name="button_scrap" type="object" string="Scrap" attrs="{'invisible': ['|', ('state', 'in', ('cancel', 'draft')), ('is_locked', '=', False)]}"/>
|
||||
<button name="button_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,assigned,done"/>
|
||||
@@ -69,7 +82,6 @@
|
||||
<sheet>
|
||||
<field name="reservation_state" invisible="1"/>
|
||||
<field name="is_locked" invisible="1"/>
|
||||
<field name="post_visible" invisible="1"/>
|
||||
<field name="qty_produced" invisible="1"/>
|
||||
<field name="unreserve_visible" invisible="1"/>
|
||||
<field name="reserve_visible" invisible="1"/>
|
||||
@@ -136,7 +148,7 @@
|
||||
<span class='text-bf'>To Produce</span>
|
||||
</div>
|
||||
<label for="lot_producing_id" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}"/>
|
||||
<div class="o_row">
|
||||
<div class="o_row" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}">
|
||||
<field name="lot_producing_id"
|
||||
context="{'default_product_id': product_id, 'default_company_id': company_id}" attrs="{'invisible': [('product_tracking', 'in', ('none', False))]}"/>
|
||||
<button name="action_generate_serial" type="object" class="btn btn-primary fa fa-plus-square-o" aria-label="Creates a new serial/lot number" title="Creates a new serial/lot number" role="img" attrs="{'invisible': ['|', ('product_tracking', 'in', ('none', False)), ('lot_producing_id', '!=', False)]}"/>
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import change_production_qty
|
||||
from . import stock_warn_insufficient_qty
|
||||
from . import mrp_production_backorder
|
||||
from . import mrp_consumption_warning
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class MrpConsumptionWarning(models.TransientModel):
|
||||
_name = 'mrp.consumption.warning'
|
||||
_description = "Wizard in case of consumption in warning/strict and more component has been used for a MO (related to the bom)"
|
||||
|
||||
mrp_production_ids = fields.Many2many('mrp.production')
|
||||
mrp_production_count = fields.Integer(compute="_compute_mrp_production_count")
|
||||
|
||||
consumption = fields.Selection([
|
||||
('flexible', 'Allowed'),
|
||||
('warning', 'Allowed with warning'),
|
||||
('strict', 'Blocked')], compute="_compute_consumption")
|
||||
mrp_consumption_warning_line_ids = fields.One2many('mrp.consumption.warning.line', 'mrp_consumption_warning_id')
|
||||
|
||||
@api.depends("mrp_production_ids")
|
||||
def _compute_mrp_production_count(self):
|
||||
for wizard in self:
|
||||
wizard.mrp_production_count = len(wizard.mrp_production_ids)
|
||||
|
||||
@api.depends("mrp_consumption_warning_line_ids.consumption")
|
||||
def _compute_consumption(self):
|
||||
for wizard in self:
|
||||
consumption_map = set(wizard.mrp_consumption_warning_line_ids.mapped("consumption"))
|
||||
wizard.consumption = "strict" in consumption_map and "strict" or "warning" in consumption_map and "warning" or "flexible"
|
||||
|
||||
def action_confirm(self):
|
||||
return self.mrp_production_ids.with_context(skip_consumption=True).button_mark_done()
|
||||
|
||||
|
||||
class MrpConsumptionWarningLine(models.TransientModel):
|
||||
_name = 'mrp.consumption.warning.line'
|
||||
_description = "Line of issue consumption"
|
||||
|
||||
mrp_consumption_warning_id = fields.Many2one('mrp.consumption.warning', "Parent Wizard", readonly=True, required=True, ondelete="cascade")
|
||||
mrp_production_id = fields.Many2one('mrp.production', "Manufacturing Order", readonly=True, required=True, ondelete="cascade")
|
||||
consumption = fields.Selection(related="mrp_production_id.consumption")
|
||||
|
||||
product_id = fields.Many2one('product.product', "Product", readonly=True, required=True)
|
||||
product_uom_id = fields.Many2one('uom.uom', "Unit of Measure", related="product_id.uom_id", readonly=True)
|
||||
product_consumed_qty_uom = fields.Float("Consumed", readonly=True)
|
||||
product_expected_qty_uom = fields.Float("To Consume", readonly=True)
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- MO Consumption Warning -->
|
||||
<record id="view_mrp_consumption_warning_form" model="ir.ui.view">
|
||||
<field name="name">Consumption Warning</field>
|
||||
<field name="model">mrp.consumption.warning</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Consumption Warning">
|
||||
<field name="mrp_production_ids" invisible="1"/>
|
||||
<field name="consumption" invisible="1"/>
|
||||
<field name="mrp_production_count" invisible="1"/>
|
||||
<div class="m-2">
|
||||
You consumed a different quantity than expected for the following products.
|
||||
<b attrs="{'invisible': [('consumption', '=', 'strict')]}">
|
||||
Please confirm it has been done on purpose.
|
||||
</b>
|
||||
<b attrs="{'invisible': [('consumption', '!=', 'strict')]}">
|
||||
Please review your component consumption or ask a manager to validate
|
||||
<span attrs="{'invisible':[('mrp_production_count', '!=', 1)]}">this manufacturing order</span>
|
||||
<span attrs="{'invisible':[('mrp_production_count', '=', 1)]}">these manufacturing orders</span>.
|
||||
</b>
|
||||
</div>
|
||||
<field name="mrp_consumption_warning_line_ids" nolabel="1">
|
||||
<tree create="0" delete="0" editable="top">
|
||||
<field name="mrp_production_id" attrs="{'column_invisible':[('parent.mrp_production_count', '=', 1)]}" force_save="1"/>
|
||||
<field name="consumption" invisible="1" force_save="1"/>
|
||||
<field name="product_id" force_save="1"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom" force_save="1"/>
|
||||
<field name="product_expected_qty_uom" force_save="1"/>
|
||||
<field name="product_consumed_qty_uom" force_save="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
<footer>
|
||||
<button name="action_confirm" string="Force"
|
||||
groups="mrp.group_mrp_manager" attrs="{'invisible': [('consumption', '!=', 'strict')]}"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
<button name="action_confirm" string="Confirm" attrs="{'invisible': [('consumption', '=', 'strict')]}"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
<button special="cancel" string="Review Consumption"
|
||||
colspan="1" type="object" class="btn-primary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mrp_consumption_warning" model="ir.actions.act_window">
|
||||
<field name="name">Consumption Warning</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">mrp.consumption.warning</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -71,7 +71,6 @@ class MrpProduction(models.Model):
|
||||
AccountAnalyticLine.create(vals)
|
||||
|
||||
def button_mark_done(self):
|
||||
self.ensure_one()
|
||||
res = super(MrpProduction, self).button_mark_done()
|
||||
for order in self:
|
||||
order._costs_generate()
|
||||
|
||||
@@ -78,7 +78,7 @@ class TestMrpAccount(TestWorkOrderProcessCommon):
|
||||
mo_form = Form(production_table)
|
||||
mo_form.qty_producing = 1
|
||||
production_table = mo_form.save()
|
||||
production_table.post_inventory()
|
||||
production_table._post_inventory()
|
||||
move_value = production_table.move_finished_ids.filtered(lambda x: x.state == "done").stock_valuation_layer_ids.value
|
||||
|
||||
# 1 table head at 20 + 4 table leg at 15 + 4 bolt at 10 + 10 screw at 10 + 1*20 (extra cost)
|
||||
|
||||
@@ -55,7 +55,10 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
mo.post_inventory()
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_backorder()
|
||||
mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 10)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
@@ -93,7 +96,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._produce(mo, 1)
|
||||
action = mo.button_mark_done()
|
||||
backorder = Form(self.env['mrp.production.backorder'].with_context(**action['context']))
|
||||
backorder.save().action_close_mo()
|
||||
backorder.save().action_backorder()
|
||||
mo = mo.procurement_group_id.mrp_production_ids[-1]
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 10)
|
||||
@@ -131,7 +134,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
mo.post_inventory()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 20)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
@@ -169,7 +172,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
mo.post_inventory()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 8.8)
|
||||
self.assertEqual(self.product1.value_svl, 8.8)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
@@ -208,7 +211,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._make_in_move(self.component, 1)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
mo.post_inventory()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 8.8)
|
||||
self.assertEqual(self.product1.value_svl, 7.2)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
@@ -246,7 +249,7 @@ class TestMrpValuationStandard(TestMrpValuationCommon):
|
||||
self._make_in_move(self.component, 1, 20)
|
||||
mo = self._make_mo(self.bom, 2)
|
||||
self._produce(mo, 1)
|
||||
mo.post_inventory()
|
||||
mo._post_inventory()
|
||||
self.assertEqual(self.component.value_svl, 15)
|
||||
self.assertEqual(self.product1.value_svl, 15)
|
||||
self.assertEqual(self.component.quantity_svl, 1)
|
||||
|
||||
@@ -49,6 +49,7 @@ class TestStockProductionLot(TestStockCommon):
|
||||
'product_tmpl_id': cls.product_apple_pie.product_tmpl_id.id,
|
||||
'product_uom_id': cls.uom_unit.id,
|
||||
'product_qty': 1.0,
|
||||
'consumption': 'flexible',
|
||||
'type': 'normal',
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': cls.product_apple.id, 'product_qty': 3}),
|
||||
|
||||
@@ -83,7 +83,7 @@ class StockPicking(models.Model):
|
||||
productions |= production
|
||||
for subcontracted_production in productions:
|
||||
if subcontracted_production.state == 'progress':
|
||||
subcontracted_production.post_inventory()
|
||||
subcontracted_production._post_inventory()
|
||||
else:
|
||||
subcontracted_production.button_mark_done()
|
||||
# For concistency, set the date on production move before the date
|
||||
|
||||
@@ -434,7 +434,7 @@ class TestSaleMrpFlow(AccountTestCommon):
|
||||
mo_form = Form(mnf_product_d)
|
||||
mo_form.qty_producing = 20
|
||||
mnf_product_d = mo_form.save()
|
||||
mnf_product_d.post_inventory()
|
||||
mnf_product_d._post_inventory()
|
||||
|
||||
# Check state of manufacturing order.
|
||||
self.assertEqual(mnf_product_d.state, 'done', 'Manufacturing order should still be in progress state.')
|
||||
@@ -484,7 +484,7 @@ class TestSaleMrpFlow(AccountTestCommon):
|
||||
mo_form = Form(mnf_product_a)
|
||||
mo_form.qty_producing = mo_form.product_qty
|
||||
mnf_product_a = mo_form.save()
|
||||
mnf_product_a.post_inventory()
|
||||
mnf_product_a._post_inventory()
|
||||
# Check state of manufacturing order product A.
|
||||
self.assertEqual(mnf_product_a.state, 'done', 'Manufacturing order should still be in the progress state.')
|
||||
# Check product A avaialble quantity should be 120.
|
||||
|
||||
Reference in New Issue
Block a user