From 45e53ff7a599f87bf98fc67934ebf083c2dcaee1 Mon Sep 17 00:00:00 2001 From: Arnaud Baes Date: Tue, 4 Dec 2018 12:27:37 +0000 Subject: [PATCH] [IMP] mrp, stock: route applied on kit's components With this commit, the routes of the components of a kit are applied instead of the routes on the kit itself. To do this, a refactor of '_adjust_procure_method' was made in order to use it on stock.moves The method '_adjust_procure_method' was moved to the stock module in order to be used in a future task concerning repair orders ( TaskID: 1887961). TaskID: 1863856 --- addons/mrp/models/mrp_production.py | 20 +-- addons/mrp/models/stock_move.py | 4 +- addons/mrp/models/stock_rule.py | 26 +++ addons/sale_mrp/tests/test_sale_mrp_flow.py | 168 +++++++++++++++++--- addons/stock/models/stock_move.py | 25 +++ 5 files changed, 205 insertions(+), 38 deletions(-) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 10ac3e19112..c6ca7dd25d0 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -513,24 +513,6 @@ class MrpProduction(models.Model): else: return self.location_src_id - @api.multi - def _adjust_procure_method(self): - try: - mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', _('Make To Order')) - except: - mto_route = False - for move in self.move_raw_ids: - product = move.product_id - routes = product.route_ids + product.route_from_categ_ids + move.warehouse_id.route_ids - # TODO: optimize with read_group? - pull = self.env['stock.rule'].search([('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id), - ('location_id', '=', move.location_dest_id.id), ('action', '!=', 'push')], limit=1) - if pull and (pull.procure_method == 'make_to_order'): - move.procure_method = pull.procure_method - elif not pull: # If there is no make_to_stock rule either - if mto_route and mto_route.id in [x.id for x in routes]: - move.procure_method = 'make_to_order' - @api.multi def _update_raw_move(self, bom_line, line_data): """ :returns update_move, old_quantity, new_quantity """ @@ -581,7 +563,7 @@ class MrpProduction(models.Model): 'unit_factor': move_raw.product_uom_qty / production.product_qty }) production._generate_finished_moves() - production._adjust_procure_method() + production.move_raw_ids._adjust_procure_method() (production.move_raw_ids | production.move_finished_ids)._action_confirm() return True diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 096074b8157..2234d34f5f4 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -201,7 +201,9 @@ class StockMove(models.Model): def _generate_move_phantom(self, bom_line, product_qty, quantity_done): if bom_line.product_id.type in ['product', 'consu']: - return self.copy(default=self._prepare_phantom_move_values(bom_line, product_qty, quantity_done)) + move = self.copy(default=self._prepare_phantom_move_values(bom_line, product_qty, quantity_done)) + move._adjust_procure_method() + return move return self.env['stock.move'] def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False): diff --git a/addons/mrp/models/stock_rule.py b/addons/mrp/models/stock_rule.py index 8b8a252d793..d66d1af3298 100644 --- a/addons/mrp/models/stock_rule.py +++ b/addons/mrp/models/stock_rule.py @@ -90,3 +90,29 @@ class StockRule(models.Model): new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(move_to_copy, new_date) new_move_vals['production_id'] = False return new_move_vals + +class ProcurementGroup(models.Model): + _inherit = 'procurement.group' + + @api.model + def run(self, product_id, product_qty, product_uom, location_id, name, origin, values): + """ If 'run' is called on a kit, this override is made in order to call + the original 'run' method with the values of the components of that kit. + """ + bom_kit = self.env['mrp.bom']._bom_find(product=product_id, bom_type='phantom') + if bom_kit: + order_qty = product_uom._compute_quantity(product_qty, bom_kit.product_uom_id, round=False) + qty_to_produce = ( order_qty / bom_kit.product_qty) + boms, bom_sub_lines = bom_kit.explode(product_id, qty_to_produce) + for bom_line, bom_line_data in bom_sub_lines: + procurement_uom = bom_line.product_uom_id + quant_uom = bom_line.product_id.uom_id + component_qty = bom_line_data['qty'] + get_param = self.env['ir.config_parameter'].sudo().get_param + if procurement_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1': + component_qty = bom_line.product_uom_id._compute_quantity(bom_line_data['qty'], quant_uom, rounding_method='HALF-UP') + procurement_uom = quant_uom + super(ProcurementGroup, self).run(bom_line.product_id, component_qty, procurement_uom, location_id, name, origin, values) + return True + else: + return super(ProcurementGroup, self).run(product_id, product_qty, product_uom, location_id, name, origin, values) diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 136d69d14ac..5b23d94dfb1 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -5,6 +5,7 @@ from datetime import datetime from odoo.tests import common, Form from odoo.exceptions import UserError +from odoo.tools import mute_logger class TestSaleMrpFlow(common.TransactionCase): @@ -25,19 +26,6 @@ class TestSaleMrpFlow(common.TransactionCase): self.stock_location = self.env.ref('stock.stock_location_stock') self.warehouse = self.env.ref('stock.warehouse0') - def test_00_sale_mrp_flow(self): - """ Test sale to mrp flow with diffrent unit of measure.""" - def create_product(name, uom_id, routes=()): - p = Form(self.env['product.product']) - p.name = name - p.type = 'product' - p.uom_id = uom_id - p.uom_po_id = uom_id - p.route_ids.clear() - for r in routes: - p.route_ids.add(r) - return p.save() - self.uom_kg = self.env['uom.uom'].search([('category_id', '=', self.categ_kgm.id), ('uom_type', '=', 'reference')], limit=1) self.uom_kg.write({ 'name': 'Test-KG', @@ -51,7 +39,7 @@ class TestSaleMrpFlow(common.TransactionCase): self.uom_unit = self.env['uom.uom'].search([('category_id', '=', self.categ_unit.id), ('uom_type', '=', 'reference')], limit=1) self.uom_unit.write({ 'name': 'Test-Unit', - 'rounding': 1.0}) + 'rounding': 0.01}) self.uom_dozen = self.UoM.create({ 'name': 'Test-DozenA', 'category_id': self.categ_unit.id, @@ -59,14 +47,29 @@ class TestSaleMrpFlow(common.TransactionCase): 'uom_type': 'bigger', 'rounding': 0.001}) + def create_product(self, name, uom_id, routes=()): + p = Form(self.env['product.product']) + p.name = name + p.type = 'product' + p.uom_id = uom_id + p.uom_po_id = uom_id + p.route_ids.clear() + for r in routes: + p.route_ids.add(r) + return p.save() + + def test_00_sale_mrp_flow(self): + """ Test sale to mrp flow with diffrent unit of measure.""" + + # Create product A, B, C, D. # -------------------------- route_manufacture = self.warehouse.manufacture_pull_id.route_id route_mto = self.warehouse.mto_pull_id.route_id - product_a = create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto]) - product_c = create_product('Product C', self.uom_kg) - product_b = create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto]) - product_d = create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto]) + product_a = self.create_product('Product A', self.uom_unit, routes=[route_manufacture, route_mto]) + product_c = self.create_product('Product C', self.uom_kg) + product_b = self.create_product('Product B', self.uom_dozen, routes=[route_manufacture, route_mto]) + product_d = self.create_product('Product D', self.uom_unit, routes=[route_manufacture, route_mto]) # ------------------------------------------------------------------------------------------ # Bill of materials for product A, B, D. @@ -489,3 +492,132 @@ class TestSaleMrpFlow(common.TransactionCase): # Check that the cost of Good Sold entries are equal to 2* (2 * 20 + 1 * 10) = 100 self.assertEqual(aml_expense.debit, 100, "Cost of Good Sold entry missing or mismatching") self.assertEqual(aml_output.credit, 100, "Cost of Good Sold entry missing or mismatching") + + @mute_logger('odoo.tests.common.onchange') + def test_10_sale_mrp_kits_routes(self): + + # Create a kit 'kit_1' : + # ----------------------- + # + # kit_1 --|- component_shelf1 x3 + # |- component_shelf2 x2 + + kit_1 = self.create_product('Kit1', self.uom_unit) + component_shelf1 = self.create_product('Comp Shelf1', self.uom_unit) + component_shelf2 = self.create_product('Comp Shelf2', self.uom_unit) + + with Form(self.env['mrp.bom']) as bom: + bom.product_tmpl_id = kit_1.product_tmpl_id + bom.product_qty = 1 + bom.product_uom_id = self.uom_unit + bom.type = 'phantom' + with bom.bom_line_ids.new() as line: + line.product_id = component_shelf1 + line.product_qty = 3 + line.product_uom_id = self.uom_unit + with bom.bom_line_ids.new() as line: + line.product_id = component_shelf2 + line.product_qty = 2 + line.product_uom_id = self.uom_unit + + # Creating 2 specific routes for each of the components of the kit + route_shelf1 = self.env['stock.location.route'].create({ + 'name': 'Shelf1 -> Customer', + 'product_selectable': True, + 'rule_ids': [(0, 0, { + 'name': 'Shelf1 -> Customer', + 'action': 'pull', + 'picking_type_id': self.ref('stock.picking_type_in'), + 'location_src_id': self.ref('stock.stock_location_components'), + 'location_id': self.ref('stock.stock_location_customers'), + })], + }) + + route_shelf2 = self.env['stock.location.route'].create({ + 'name': 'Shelf2 -> Customer', + 'product_selectable': True, + 'rule_ids': [(0, 0, { + 'name': 'Shelf2 -> Customer', + 'action': 'pull', + 'picking_type_id': self.ref('stock.picking_type_in'), + 'location_src_id': self.ref('stock.stock_location_14'), + 'location_id': self.ref('stock.stock_location_customers'), + })], + }) + + component_shelf1.write({ + 'route_ids': [(4, route_shelf1.id)]}) + component_shelf2.write({ + 'route_ids': [(4, route_shelf2.id)]}) + + # Creating a sale order for 5 kits and confirming it + order_form = Form(self.env['sale.order']) + order_form.partner_id = self.env.ref('base.res_partner_2') + with order_form.order_line.new() as line: + line.product_id = kit_1 + line.product_uom = self.uom_unit + line.product_uom_qty = 5 + order = order_form.save() + order.action_confirm() + + # Now we check that the routes of the components were applied, in order to make sure the routes set + # on the kit itself are ignored + self.assertEquals(len(order.picking_ids), 2) + self.assertEquals(len(order.picking_ids[0].move_lines), 1) + self.assertEquals(len(order.picking_ids[1].move_lines), 1) + moves = order.picking_ids.mapped('move_lines') + move_shelf1 = moves.filtered(lambda m: m.product_id == component_shelf1) + move_shelf2 = moves.filtered(lambda m: m.product_id == component_shelf2) + self.assertEquals(move_shelf1.location_id.id, self.ref('stock.stock_location_components')) + self.assertEquals(move_shelf1.location_dest_id.id, self.ref('stock.stock_location_customers')) + self.assertEquals(move_shelf2.location_id.id, self.ref('stock.stock_location_14')) + self.assertEquals(move_shelf2.location_dest_id.id, self.ref('stock.stock_location_customers')) + + @mute_logger('odoo.tests.common.onchange') + def test_11_sale_mrp_explode_kits_uom_quantities(self): + + # Create a kit 'kit_1' : + # ----------------------- + # + # 2x Dozens kit_1 --|- component_unit x6 Units + # |- component_kg x7 Kg + + kit_1 = self.create_product('Kit1', self.uom_unit) + component_unit = self.create_product('Comp Unit', self.uom_unit) + component_kg = self.create_product('Comp Kg', self.uom_kg) + + with Form(self.env['mrp.bom']) as bom: + bom.product_tmpl_id = kit_1.product_tmpl_id + bom.product_qty = 2 + bom.product_uom_id = self.uom_dozen + bom.type = 'phantom' + with bom.bom_line_ids.new() as line: + line.product_id = component_unit + line.product_qty = 6 + line.product_uom_id = self.uom_unit + with bom.bom_line_ids.new() as line: + line.product_id = component_kg + line.product_qty = 7 + line.product_uom_id = self.uom_kg + + # Creating a sale order for 3 Units of kit_1 and confirming it + order_form = Form(self.env['sale.order']) + order_form.partner_id = self.env.ref('base.res_partner_2') + with order_form.order_line.new() as line: + line.product_id = kit_1 + line.product_uom = self.uom_unit + line.product_uom_qty = 2 + order = order_form.save() + order.action_confirm() + + # Now we check that the routes of the components were applied, in order to make sure the routes set + # on the kit itself are ignored + self.assertEquals(len(order.picking_ids), 1) + self.assertEquals(len(order.picking_ids[0].move_lines), 2) + + # Finally, we check the quantities for each component on the picking + move_component_unit = order.picking_ids[0].move_lines.filtered(lambda m: m.product_id == component_unit) + move_component_kg = order.picking_ids[0].move_lines - move_component_unit + self.assertEquals(move_component_unit.product_uom_qty, 0.5) + self.assertEquals(move_component_kg.product_uom_qty, 0.583) + diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 4c02adf6400..9260b906289 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1268,3 +1268,28 @@ class StockMove(models.Model): vals = self._prepare_move_line_vals(quantity=0) vals['qty_done'] = qty ml = self.env['stock.move.line'].create(vals) + + def _adjust_procure_method(self): + """ This method will try to apply the procure method MTO on some moves if + a compatible MTO route is found. Else the procure method will be set to MTS + """ + try: + mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', + 'Make To Order') + except ValueError: + mto_route = False + for move in self: + product_id = move.product_id + domain = [ + ('location_src_id', '=', move.location_id.id), + ('location_id', '=', move.location_dest_id.id), + ('action', '!=', 'push') + ] + rules = self.env['procurement.group']._search_rule(False, product_id, move.warehouse_id, domain) + routes = product_id.route_ids + product_id.route_from_categ_ids + move.warehouse_id.route_ids + if rules and (rules.procure_method == 'make_to_order'): + move.procure_method = rules.procure_method + elif mto_route and mto_route.id in [x.id for x in routes]: + move.procure_method = 'make_to_order' + else: + move.procure_method = 'make_to_stock'