diff --git a/mrp_multi_level/README.rst b/mrp_multi_level/README.rst index 1b79418f458..f98997dc31f 100644 --- a/mrp_multi_level/README.rst +++ b/mrp_multi_level/README.rst @@ -78,15 +78,16 @@ To launch replenishment orders (moves, purchases, production orders...): hand side gears in any record. #. On the wizard, check everything is ok and click *Execute*. -Known issues / Roadmap -====================== - -* The functionality related to field *Nbr. Days* in products is not - functional for the time being. Please, stay tuned to future updates. - Changelog ========= +11.0.2.1.0 (2019-04-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [IMP] Implement *Nbr. Days* functionality to be able to group demand when + generating supply proposals. + (`#345 `_): + 11.0.2.0.0 (2018-11-20) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/mrp_multi_level/__manifest__.py b/mrp_multi_level/__manifest__.py index f32d72a2075..be4db8065de 100644 --- a/mrp_multi_level/__manifest__.py +++ b/mrp_multi_level/__manifest__.py @@ -1,9 +1,9 @@ # Copyright 2016 Ucamco - Wim Audenaert -# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# Copyright 2016-19 Eficent Business and IT Consulting Services S.L. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { 'name': 'MRP Multi Level', - 'version': '11.0.2.0.0', + 'version': '11.0.2.1.0', 'development_status': 'Beta', 'license': 'AGPL-3', 'author': 'Ucamco, ' diff --git a/mrp_multi_level/readme/HISTORY.rst b/mrp_multi_level/readme/HISTORY.rst index 03ff5468e7c..5bfd1ed9506 100644 --- a/mrp_multi_level/readme/HISTORY.rst +++ b/mrp_multi_level/readme/HISTORY.rst @@ -1,3 +1,10 @@ +11.0.2.1.0 (2019-04-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [IMP] Implement *Nbr. Days* functionality to be able to group demand when + generating supply proposals. + (`#345 `_): + 11.0.2.0.0 (2018-11-20) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/mrp_multi_level/readme/ROADMAP.rst b/mrp_multi_level/readme/ROADMAP.rst deleted file mode 100644 index b3ae9a0e80d..00000000000 --- a/mrp_multi_level/readme/ROADMAP.rst +++ /dev/null @@ -1,2 +0,0 @@ -* The functionality related to field *Nbr. Days* in products is not - functional for the time being. Please, stay tuned to future updates. diff --git a/mrp_multi_level/static/description/index.html b/mrp_multi_level/static/description/index.html index 8fc23d95215..a584a53aad6 100644 --- a/mrp_multi_level/static/description/index.html +++ b/mrp_multi_level/static/description/index.html @@ -386,40 +386,40 @@

Key Features

Table of contents

-

Configuration

+

Configuration

-

MRP Areas

+

MRP Areas

  • Go to Manufacturing > Configuration > MRP Areas and define or edit any existing area. You can specify the working hours for every area.
-

Product MRP Area Parameters

+

Product MRP Area Parameters

  • Go to Manufacturing > Master Data > Product MRP Area Parameters and set the MRP parameters for a given product and area.
  • @@ -427,7 +427,7 @@

    Product MRP Area Parameters

-

Usage

+

Usage

To manually run the MRP scheduler:

  1. Go to Manufacturing > Operations > Run MRP Multi Level.
  2. @@ -442,17 +442,18 @@

    Usage

  3. On the wizard, check everything is ok and click Execute.
-
-

Known issues / Roadmap

+
+

Changelog

+
+

11.0.2.1.0 (2019-04-02)

    -
  • The functionality related to field Nbr. Days in products is not -functional for the time being. Please, stay tuned to future updates.
  • +
  • [IMP] Implement Nbr. Days functionality to be able to group demand when +generating supply proposals. +(#345):
-
-

Changelog

-
-

11.0.2.0.0 (2018-11-20)

+
+

11.0.2.0.0 (2018-11-20)

-
-

11.0.1.1.0 (2018-08-30)

+
+

11.0.1.1.0 (2018-08-30)

  • [FIX] Consider Qty Multiple on product to propose the quantity to procure. (#297)
-
-

11.0.1.0.1 (2018-08-03)

+
+

11.0.1.0.1 (2018-08-03)

-
-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed @@ -497,16 +498,16 @@

Bug Tracker

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Ucamco
  • Eficent
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose diff --git a/mrp_multi_level/tests/test_mrp_multi_level.py b/mrp_multi_level/tests/test_mrp_multi_level.py index c417a1fe1eb..0dc2cf1b00d 100644 --- a/mrp_multi_level/tests/test_mrp_multi_level.py +++ b/mrp_multi_level/tests/test_mrp_multi_level.py @@ -1,4 +1,4 @@ -# Copyright 2018 Eficent Business and IT Consulting Services S.L. +# Copyright 2018-19 Eficent Business and IT Consulting Services S.L. # (http://www.eficent.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). @@ -17,6 +17,8 @@ def setUpClass(cls): cls.mo_obj = cls.env['mrp.production'] cls.po_obj = cls.env['purchase.order'] cls.product_obj = cls.env['product.product'] + cls.loc_obj = cls.env['stock.location'] + cls.mrp_area_obj = cls.env['mrp.area'] cls.product_mrp_area_obj = cls.env['product.mrp.area'] cls.partner_obj = cls.env['res.partner'] cls.stock_picking_obj = cls.env['stock.picking'] @@ -45,6 +47,18 @@ def setUpClass(cls): # Partner: vendor1 = cls.partner_obj.create({'name': 'Vendor 1'}) + # Create secondary location and MRP Area: + cls.sec_loc = cls.loc_obj.create({ + 'name': 'Test location', + 'usage': 'internal', + 'location_id': cls.wh.view_location_id.id, + }) + cls.secondary_area = cls.mrp_area_obj.create({ + 'name': 'Test', + 'warehouse_id': cls.wh.id, + 'location_id': cls.sec_loc.id, + }) + # Create products: route_buy = cls.env.ref('purchase.route_warehouse0_buy').id cls.prod_test = cls.product_obj.create({ @@ -59,6 +73,12 @@ def setUpClass(cls): 'product_id': cls.prod_test.id, 'mrp_area_id': cls.mrp_area.id, }) + # Parameters in secondary area with nbr_days set. + cls.product_mrp_area_obj.create({ + 'product_id': cls.prod_test.id, + 'mrp_area_id': cls.secondary_area.id, + 'mrp_nbr_days': 7, + }) cls.prod_min = cls.product_obj.create({ 'name': 'Product with minimum order qty', 'type': 'product', @@ -239,6 +259,8 @@ def setUpClass(cls): qty += 70.0 cls._create_demand_estimate( cls.prod_test, cls.stock_location, dr, qty) + cls._create_demand_estimate( + cls.prod_test, cls.sec_loc, dr, qty) cls.mrp_multi_level_wiz.create({}).run_mrp_multi_level() @@ -425,11 +447,13 @@ def test_05_moves_extra_info(self): def test_06_demand_estimates(self): """Tests demand estimates integration.""" - estimates = self.estimate_obj.search( - [('product_id', '=', self.prod_test.id)]) + estimates = self.estimate_obj.search([ + ('product_id', '=', self.prod_test.id), + ('location_id', '=', self.stock_location.id)]) self.assertEqual(len(estimates), 3) moves = self.mrp_move_obj.search([ ('product_id', '=', self.prod_test.id), + ('mrp_area_id', '=', self.mrp_area.id), ]) # 3 weeks - 3 days in the past = 18 days of valid estimates: moves_from_estimates = moves.filtered(lambda m: m.mrp_type == 'd') @@ -440,6 +464,9 @@ def test_06_demand_estimates(self): self.assertIn(-50.0, quantities) # 350 a week => 50.0 dayly: actions = moves.filtered(lambda m: m.mrp_action == 'po') self.assertEqual(len(actions), 18) + inventories = self.mrp_inventory_obj.search([ + ('mrp_area_id', '=', self.secondary_area.id)]) + self.assertEqual(len(inventories), 18) def test_07_procure_mo(self): """Test procurement wizard with MOs.""" @@ -483,5 +510,29 @@ def test_08_adjust_qty_to_order(self): ('product_mrp_area_id.product_id', '=', self.prod_multiple.id)]) self.assertEqual(mrp_inv_multiple.to_procure, 125) + def test_09_group_demand(self): + """Test demand grouping functionality, `nbr_days`.""" + estimates = self.estimate_obj.search([ + ('product_id', '=', self.prod_test.id), + ('location_id', '=', self.sec_loc.id)]) + self.assertEqual(len(estimates), 3) + moves = self.mrp_move_obj.search([ + ('product_id', '=', self.prod_test.id), + ('mrp_area_id', '=', self.secondary_area.id), + ]) + # 3 weeks - 3 days in the past = 18 days of valid estimates: + moves_from_estimates = moves.filtered(lambda m: m.mrp_type == 'd') + self.assertEqual(len(moves_from_estimates), 18) + # 18 days of demand / 7 nbr_days = 2.57 => 3 supply moves expected. + supply_moves = moves.filtered(lambda m: m.mrp_type == 's') + self.assertEqual(len(supply_moves), 3) + quantities = supply_moves.mapped('mrp_qty') + week_1_expected = sum(moves_from_estimates[0:7].mapped('mrp_qty')) + self.assertIn(abs(week_1_expected), quantities) + week_2_expected = sum(moves_from_estimates[7:14].mapped('mrp_qty')) + self.assertIn(abs(week_2_expected), quantities) + week_3_expected = sum(moves_from_estimates[14:].mapped('mrp_qty')) + self.assertIn(abs(week_3_expected), quantities) + # TODO: test procure wizard: pos, multiple... # TODO: test multiple destination IDS:... diff --git a/mrp_multi_level/wizards/mrp_multi_level.py b/mrp_multi_level/wizards/mrp_multi_level.py index 2143d0ca52e..67b3b1f8b56 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -1,6 +1,7 @@ -# © 2016 Ucamco - Wim Audenaert -# Copyright 2016-18 Eficent Business and IT Consulting Services S.L. +# Copyright 2016 Ucamco - Wim Audenaert +# Copyright 2016-19 Eficent Business and IT Consulting Services S.L. # - Jordi Ballester Alomar +# - Lois Rilo # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models, exceptions, _ @@ -507,64 +508,48 @@ def _init_mrp_move_grouped_demand(self, nbr_create, product_mrp_area): last_date = None last_qty = 0.00 onhand = product_mrp_area.qty_available - move_ids = [] - for move in product_mrp_area.mrp_move_ids: - move_ids.append(move.id) - for move_id in move_ids: - move_rec = self.env['mrp.move'].search( - [('id', '=', move_id)]) - for move in move_rec: - if move.mrp_action == 'none': - if last_date is not None: - if datetime.date( - datetime.strptime( - move.mrp_date, '%Y-%m-%d')) \ - > last_date+timedelta( - days=product_mrp_area.mrp_nbr_days): - if (onhand + last_qty + move.mrp_qty) \ - < product_mrp_area.mrp_minimum_stock \ - or (onhand + last_qty) \ - < product_mrp_area.mrp_minimum_stock: - name = 'Grouped Demand for %d Days' % \ - product_mrp_area.mrp_nbr_days - qtytoorder = \ - product_mrp_area.mrp_minimum_stock - \ - product_mrp_area - last_qty - cm = self.create_move( - product_mrp_area_id=product_mrp_area, - mrp_date=last_date, - mrp_qty=qtytoorder, - name=name) - qty_ordered = cm['qty_ordered'] - onhand = onhand + last_qty + qty_ordered - last_date = None - last_qty = 0.00 - nbr_create += 1 - if (onhand + last_qty + move.mrp_qty) < \ - product_mrp_area.mrp_minimum_stock or \ - (onhand + last_qty) < \ - product_mrp_area.mrp_minimum_stock: - if last_date is None: - last_date = datetime.date( - datetime.strptime(move.mrp_date, - '%Y-%m-%d')) - last_qty = move.mrp_qty - else: - last_qty = last_qty + move.mrp_qty - else: - last_date = datetime.date( - datetime.strptime(move.mrp_date, - '%Y-%m-%d')) - onhand = onhand + move.mrp_qty - - if last_date is not None and last_qty != 0.00: - name = 'Grouped Demand for %d Days' % \ - (product_mrp_area.mrp_nbr_days, ) + grouping_delta = product_mrp_area.mrp_nbr_days + for move in product_mrp_area.mrp_move_ids.filtered( + lambda m: m.mrp_action == 'none'): + if last_date and ( + fields.Date.from_string(move.mrp_date) + >= last_date + timedelta(days=grouping_delta)) and ( + (onhand + last_qty + move.mrp_qty) + < product_mrp_area.mrp_minimum_stock + or (onhand + last_qty) + < product_mrp_area.mrp_minimum_stock): + name = 'Grouped Demand for %d Days' % grouping_delta + qtytoorder = product_mrp_area.mrp_minimum_stock - last_qty + cm = self.create_move( + product_mrp_area_id=product_mrp_area, + mrp_date=last_date, + mrp_qty=qtytoorder, + name=name) + qty_ordered = cm.get('qty_ordered', 0.0) + onhand = onhand + last_qty + qty_ordered + last_date = None + last_qty = 0.00 + nbr_create += 1 + if (onhand + last_qty + move.mrp_qty) < \ + product_mrp_area.mrp_minimum_stock or \ + (onhand + last_qty) < \ + product_mrp_area.mrp_minimum_stock: + if not last_date: + last_date = fields.Date.from_string(move.mrp_date) + last_qty = move.mrp_qty + else: + last_qty += move.mrp_qty + else: + last_date = fields.Date.from_string(move.mrp_date) + onhand += move.mrp_qty + + if last_date and last_qty != 0.00: + name = 'Grouped Demand for %d Days' % grouping_delta qtytoorder = product_mrp_area.mrp_minimum_stock - onhand - last_qty cm = self.create_move( product_mrp_area_id=product_mrp_area, mrp_date=last_date, mrp_qty=qtytoorder, name=name) - qty_ordered = cm['qty_ordered'] + qty_ordered = cm.get('qty_ordered', 0.0) onhand += qty_ordered nbr_create += 1 return nbr_create @@ -605,7 +590,6 @@ def _mrp_calculation(self, mrp_lowest_llc): else: onhand += move.mrp_qty else: - # TODO: review this nbr_create = self._init_mrp_move_grouped_demand( nbr_create, product_mrp_area)