diff options
| author | Rémy Voet (ryv) <ryv@odoo.com> | 2020-05-05 15:17:06 +0200 |
|---|---|---|
| committer | Simon Lejeune <sle@openerp.com> | 2020-06-15 16:59:41 +0200 |
| commit | 5011ec520c48cab4117e6e11cc91c1d551bbf648 (patch) | |
| tree | b819fa1515aa896dbd0c04dee098e42ad8e8bb56 | |
| parent | 0e2765f5fd0ca0079f1f024ec3a1448b6d2ea3db (diff) | |
[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
25 files changed, 379 insertions, 189 deletions
diff --git a/addons/mrp/__manifest__.py b/addons/mrp/__manifest__.py index 34b915039db..ce6d450810d 100644 --- a/addons/mrp/__manifest__.py +++ b/addons/mrp/__manifest__.py @@ -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', diff --git a/addons/mrp/data/mrp_demo.xml b/addons/mrp/data/mrp_demo.xml index 3ef43e55024..fb984452588 100644 --- a/addons/mrp/data/mrp_demo.xml +++ b/addons/mrp/data/mrp_demo.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> diff --git a/addons/mrp/models/mrp_abstract_workorder.py b/addons/mrp/models/mrp_abstract_workorder.py index 54edf3e8ffb..fafd11d3c85 100644 --- a/addons/mrp/models/mrp_abstract_workorder.py +++ b/addons/mrp/models/mrp_abstract_workorder.py @@ -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, ) diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index c999122c578..6f0603c028f 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -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 ) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 1398a6f1916..76beb12b253 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -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) diff --git a/addons/mrp/security/ir.model.access.csv b/addons/mrp/security/ir.model.access.csv index 6fd221fe93b..ab13940651c 100644 --- a/addons/mrp/security/ir.model.access.csv +++ b/addons/mrp/security/ir.model.access.csv @@ -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 diff --git a/addons/mrp/tests/common.py b/addons/mrp/tests/common.py index 832a7f2e442..2992c6961d6 100644 --- a/addons/mrp/tests/common.py +++ b/addons/mrp/tests/common.py @@ -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}), diff --git a/addons/mrp/tests/test_bom.py b/addons/mrp/tests/test_bom.py index 77cadb63613..8a26d960935 100644 --- a/addons/mrp/tests/test_bom.py +++ b/addons/mrp/tests/test_bom.py @@ -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({ diff --git a/addons/mrp/tests/test_byproduct.py b/addons/mrp/tests/test_byproduct.py index e07bdea83ae..7b0ed3e0926 100644 --- a/addons/mrp/tests/test_byproduct.py +++ b/addons/mrp/tests/test_byproduct.py @@ -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!') diff --git a/addons/mrp/tests/test_cancel_mo.py b/addons/mrp/tests/test_cancel_mo.py index 97726da7d60..1bca25b3664 100644 --- a/addons/mrp/tests/test_cancel_mo.py +++ b/addons/mrp/tests/test_cancel_mo.py @@ -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): diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 3cca0512e0b..9e844270b56 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -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 - 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() + # 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') - 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() + 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 = 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: diff --git a/addons/mrp/tests/test_stock.py b/addons/mrp/tests/test_stock.py index 9b51973f19d..7ca4b2493d3 100644 --- a/addons/mrp/tests/test_stock.py +++ b/addons/mrp/tests/test_stock.py @@ -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, diff --git a/addons/mrp/tests/test_traceability.py b/addons/mrp/tests/test_traceability.py index a7f09bf6de1..ea861c2cefe 100644 --- a/addons/mrp/tests/test_traceability.py +++ b/addons/mrp/tests/test_traceability.py @@ -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}), diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index 7327ed84c11..b8d71fd4bf7 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -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': [ diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml index 323466f8c5e..c228119344b 100644 --- a/addons/mrp/views/mrp_bom_views.xml +++ b/addons/mrp/views/mrp_bom_views.xml @@ -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"/> diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 3b9159cd177..2f6fc4cbd22 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -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)]}"/> diff --git a/addons/mrp/wizard/__init__.py b/addons/mrp/wizard/__init__.py index 5047d85ede2..466142a28a5 100644 --- a/addons/mrp/wizard/__init__.py +++ b/addons/mrp/wizard/__init__.py @@ -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 diff --git a/addons/mrp/wizard/mrp_consumption_warning.py b/addons/mrp/wizard/mrp_consumption_warning.py new file mode 100644 index 00000000000..3ad16839cf8 --- /dev/null +++ b/addons/mrp/wizard/mrp_consumption_warning.py @@ -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) diff --git a/addons/mrp/wizard/mrp_consumption_warning_views.xml b/addons/mrp/wizard/mrp_consumption_warning_views.xml new file mode 100644 index 00000000000..947fccdac47 --- /dev/null +++ b/addons/mrp/wizard/mrp_consumption_warning_views.xml @@ -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> diff --git a/addons/mrp_account/models/mrp_production.py b/addons/mrp_account/models/mrp_production.py index 3a10fdacf1f..4bd0c337594 100644 --- a/addons/mrp_account/models/mrp_production.py +++ b/addons/mrp_account/models/mrp_production.py @@ -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() diff --git a/addons/mrp_account/tests/test_mrp_account.py b/addons/mrp_account/tests/test_mrp_account.py index bd9adba0a5c..f77dbfc3c6e 100644 --- a/addons/mrp_account/tests/test_mrp_account.py +++ b/addons/mrp_account/tests/test_mrp_account.py @@ -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) diff --git a/addons/mrp_account/tests/test_valuation_layers.py b/addons/mrp_account/tests/test_valuation_layers.py index a116995a0c4..df03cc163de 100644 --- a/addons/mrp_account/tests/test_valuation_layers.py +++ b/addons/mrp_account/tests/test_valuation_layers.py @@ -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) diff --git a/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py b/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py index ac0f2f3528a..22e9764d2fd 100644 --- a/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py +++ b/addons/mrp_product_expiry/tests/test_mrp_product_expiry.py @@ -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}), diff --git a/addons/mrp_subcontracting/models/stock_picking.py b/addons/mrp_subcontracting/models/stock_picking.py index 0ab1655a6dc..43cba77b1a3 100644 --- a/addons/mrp_subcontracting/models/stock_picking.py +++ b/addons/mrp_subcontracting/models/stock_picking.py @@ -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 diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 71bc229c6e2..527e992faf7 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -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. |
