From 4625b79bc938c653a15f09308d18f56ea3f1f5a6 Mon Sep 17 00:00:00 2001 From: Lois Rilo Date: Tue, 21 May 2019 12:51:37 +0200 Subject: [PATCH] [11.0][REW/IMP] mrp_multi_level: * Extract concept of planned orders from mrp.move. * Fix error grouping demand when there is no supply for a the first day of grouping. * Adapt tests. --- mrp_multi_level/README.rst | 10 +- mrp_multi_level/__manifest__.py | 2 +- mrp_multi_level/models/__init__.py | 1 + mrp_multi_level/models/mrp_area.py | 13 +- mrp_multi_level/models/mrp_inventory.py | 33 +- mrp_multi_level/models/mrp_move.py | 66 ++-- mrp_multi_level/models/mrp_planned_order.py | 60 ++++ mrp_multi_level/models/product_mrp_area.py | 33 +- mrp_multi_level/readme/DESCRIPTION.rst | 4 +- mrp_multi_level/readme/HISTORY.rst | 6 + mrp_multi_level/security/ir.model.access.csv | 2 + mrp_multi_level/static/description/index.html | 86 ++--- mrp_multi_level/tests/test_mrp_multi_level.py | 68 ++-- mrp_multi_level/views/mrp_area_views.xml | 31 +- mrp_multi_level/views/mrp_inventory_views.xml | 9 +- .../views/product_mrp_area_views.xml | 26 +- .../wizards/mrp_inventory_procure.py | 42 +-- mrp_multi_level/wizards/mrp_multi_level.py | 323 +++++++++--------- 18 files changed, 467 insertions(+), 348 deletions(-) create mode 100644 mrp_multi_level/models/mrp_planned_order.py diff --git a/mrp_multi_level/README.rst b/mrp_multi_level/README.rst index 1886afb25..5355062d1 100644 --- a/mrp_multi_level/README.rst +++ b/mrp_multi_level/README.rst @@ -35,12 +35,12 @@ and explodes this down to the lowest level. Key Features ------------ -* MRP parameters at product variant level. +* MRP parameters set by product variant MRP area pairs. * Integration with `Stock Demand Estimates `_ system. * Cron job to calculate the MRP demand. * Manually calculate the MRP demand. * Confirm the calculated MRP demand and create PO's, or MO's. -* Able to see the products for which action is needed. +* Able to see the products for which action is needed throught Planned Orders. **Table of contents** @@ -81,6 +81,12 @@ To launch replenishment orders (moves, purchases, production orders...): Changelog ========= +11.0.3.0.0 (2019-05-22) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [REW/IMP] Rework to include Planned Orders. + (`#365 `_): + 11.0.2.2.0 (2019-05-02) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/mrp_multi_level/__manifest__.py b/mrp_multi_level/__manifest__.py index 5d9252026..af8f33565 100644 --- a/mrp_multi_level/__manifest__.py +++ b/mrp_multi_level/__manifest__.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { 'name': 'MRP Multi Level', - 'version': '11.0.2.2.0', + 'version': '11.0.3.0.0', 'development_status': 'Beta', 'license': 'AGPL-3', 'author': 'Ucamco, ' diff --git a/mrp_multi_level/models/__init__.py b/mrp_multi_level/models/__init__.py index 0ca21df77..486079907 100644 --- a/mrp_multi_level/models/__init__.py +++ b/mrp_multi_level/models/__init__.py @@ -3,5 +3,6 @@ from . import stock_location from . import product_product from . import product_template from . import mrp_move +from . import mrp_planned_order from . import mrp_inventory from . import product_mrp_area diff --git a/mrp_multi_level/models/mrp_area.py b/mrp_multi_level/models/mrp_area.py index 0e400db6e..2b7c9c60c 100644 --- a/mrp_multi_level/models/mrp_area.py +++ b/mrp_multi_level/models/mrp_area.py @@ -1,15 +1,16 @@ # © 2016 Ucamco - Wim Audenaert -# © 2016 Eficent Business and IT Consulting Services S.L. +# © 2016-19 Eficent Business and IT Consulting Services S.L. # - Jordi Ballester Alomar +# - Lois Rilo Antelo # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from odoo import fields, models +from odoo import api, fields, models class MrpArea(models.Model): _name = 'mrp.area' - name = fields.Char('Name') + name = fields.Char(required=True) warehouse_id = fields.Many2one( comodel_name='stock.warehouse', string='Warehouse', required=True, @@ -24,3 +25,9 @@ class MrpArea(models.Model): string='Working Hours', related='warehouse_id.calendar_id', ) + + @api.multi + def _get_locations(self): + self.ensure_one() + return self.env['stock.location'].search([ + ('id', 'child_of', self.location_id.id)]) diff --git a/mrp_multi_level/models/mrp_inventory.py b/mrp_multi_level/models/mrp_inventory.py index 64f472888..9457161d7 100644 --- a/mrp_multi_level/models/mrp_inventory.py +++ b/mrp_multi_level/models/mrp_inventory.py @@ -1,6 +1,7 @@ # © 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. # - Jordi Ballester Alomar +# - Lois Rilo Antelo # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models @@ -17,7 +18,6 @@ class MrpInventory(models.Model): # TODO: name to pass to procurements? # TODO: compute procurement_date to pass to the wizard? not needed for # PO at least. Check for MO and moves - # TODO: show a LT based on the procure method? mrp_area_id = fields.Many2one( comodel_name='mrp.area', string='MRP Area', @@ -41,18 +41,38 @@ class MrpInventory(models.Model): supply_qty = fields.Float(string='Supply') initial_on_hand_qty = fields.Float(string='Starting Inventory') final_on_hand_qty = fields.Float(string='Forecasted Inventory') - to_procure = fields.Float(string='To procure') + to_procure = fields.Float( + string="To procure", + compute="_compute_to_procure", + store=True, + ) + running_availability = fields.Float( + string="Planned Availability", + help="Theoretical inventory level if all planned orders" + "were released.", + ) order_release_date = fields.Date( string="Order Release Date", compute="_compute_order_release_date", store=True, ) + planned_order_ids = fields.One2many( + comodel_name="mrp.planned.order", + inverse_name="mrp_inventory_id", + readonly=True, + ) @api.multi def _compute_uom_id(self): for rec in self: rec.uom_id = rec.product_mrp_area_id.product_id.uom_id + @api.depends("planned_order_ids", "planned_order_ids.qty_released") + def _compute_to_procure(self): + for rec in self: + rec.to_procure = sum(rec.planned_order_ids.mapped('mrp_qty')) - \ + sum(rec.planned_order_ids.mapped('qty_released')) + @api.multi @api.depends('product_mrp_area_id', 'product_mrp_area_id.main_supplierinfo_id', @@ -61,12 +81,7 @@ class MrpInventory(models.Model): def _compute_order_release_date(self): today = date.today() for rec in self.filtered(lambda r: r.date): - delay = 0 - if rec.product_mrp_area_id.supply_method == 'buy': - delay = rec.product_mrp_area_id.main_supplierinfo_id.delay - elif rec.product_mrp_area_id.supply_method == 'manufacture': - delay = rec.product_mrp_area_id.mrp_lead_time - # TODO: 'move' supply method + delay = rec.product_mrp_area_id.mrp_lead_time if delay and rec.mrp_area_id.calendar_id: dt_date = fields.Datetime.from_string(rec.date) order_release_date = rec.mrp_area_id.calendar_id.plan_days( diff --git a/mrp_multi_level/models/mrp_move.py b/mrp_multi_level/models/mrp_move.py index 9c14f970e..a92733954 100644 --- a/mrp_multi_level/models/mrp_move.py +++ b/mrp_multi_level/models/mrp_move.py @@ -11,56 +11,41 @@ class MrpMove(models.Model): # TODO: too many indexes... + product_mrp_area_id = fields.Many2one( + comodel_name="product.mrp.area", + string="Product", index=True, + ) mrp_area_id = fields.Many2one( - comodel_name='mrp.area', - related='product_mrp_area_id.mrp_area_id', - string='MRP Area', + comodel_name="mrp.area", + related="product_mrp_area_id.mrp_area_id", + string="MRP Area", store=True, index=True, ) + product_id = fields.Many2one( + comodel_name='product.product', + related='product_mrp_area_id.product_id', + store=True, + ) + current_date = fields.Date(string='Current Date') current_qty = fields.Float(string='Current Qty') - # TODO: cancel is not needed I think... - mrp_action = fields.Selection( - selection=[('mo', 'Manufacturing Order'), - ('po', 'Purchase Order'), - ('cancel', 'Cancel'), - ('none', 'None')], - string='Action', - ) - mrp_action_date = fields.Date(string='MRP Action Date') mrp_date = fields.Date(string='MRP Date') - mrp_move_down_ids = fields.Many2many( - comodel_name='mrp.move', - relation='mrp_move_rel', - column1='move_up_id', - column2='move_down_id', - string='MRP Move DOWN', - ) - mrp_move_up_ids = fields.Many2many( - comodel_name='mrp.move', - relation='mrp_move_rel', - column1='move_down_id', - column2='move_up_id', - string='MRP Move UP', - ) - mrp_minimum_stock = fields.Float( - string='Minimum Stock', - related='product_mrp_area_id.mrp_minimum_stock', + planned_order_up_ids = fields.Many2many( + comodel_name="mrp.planned.order", + relation="mrp_move_planned_order_rel", + column1="move_down_id", + column2="order_id", + string="Planned Orders UP", ) mrp_order_number = fields.Char(string='Order Number') - # TODO: replace by a char origin? mrp_origin = fields.Selection( selection=[('mo', 'Manufacturing Order'), ('po', 'Purchase Order'), ('mv', 'Move'), - ('fc', 'Forecast'), ('mrp', 'MRP')], + ('fc', 'Forecast'), + ('mrp', 'MRP')], string='Origin') - mrp_processed = fields.Boolean(string='Processed') - product_mrp_area_id = fields.Many2one( - comodel_name='product.mrp.area', - string='Product', index=True, - ) mrp_qty = fields.Float(string='MRP Quantity') mrp_type = fields.Selection( selection=[('s', 'Supply'), ('d', 'Demand')], @@ -68,12 +53,8 @@ class MrpMove(models.Model): ) name = fields.Char(string='Description') parent_product_id = fields.Many2one( - comodel_name='product.product', - string='Parent Product', index=True, - ) - product_id = fields.Many2one( - comodel_name='product.product', - string='Product', index=True, + comodel_name="product.product", + string="Parent Product", index=True, ) production_id = fields.Many2one( comodel_name='mrp.production', @@ -87,7 +68,6 @@ class MrpMove(models.Model): comodel_name='purchase.order', string='Purchase Order', index=True, ) - running_availability = fields.Float(string='Running Availability') state = fields.Selection( selection=[('draft', 'Draft'), ('assigned', 'Assigned'), diff --git a/mrp_multi_level/models/mrp_planned_order.py b/mrp_multi_level/models/mrp_planned_order.py new file mode 100644 index 000000000..70162980f --- /dev/null +++ b/mrp_multi_level/models/mrp_planned_order.py @@ -0,0 +1,60 @@ +# Copyright 2019 Eficent Business and IT Consulting Services S.L. +# - Lois Rilo Antelo +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import models, fields + + +class MrpPlannedOrder(models.Model): + _name = "mrp.planned.order" + _description = "Planned Order" + _order = "due_date, id" + + name = fields.Char(string="Description") + product_mrp_area_id = fields.Many2one( + comodel_name="product.mrp.area", + string="Product", + index=True, + ) + mrp_area_id = fields.Many2one( + comodel_name="mrp.area", + related="product_mrp_area_id.mrp_area_id", + string="MRP Area", + store=True, + index=True, + ) + product_id = fields.Many2one( + comodel_name="product.product", + related="product_mrp_area_id.product_id", + store=True, + ) + order_release_date = fields.Date( + string="Release Date", + help="Order release date planned by MRP.", + ) + due_date = fields.Date( + string="Due Date", + help="Date in which the supply must have been completed.", + ) + qty_released = fields.Float() + fixed = fields.Boolean() + mrp_qty = fields.Float(string="Quantity") + mrp_move_down_ids = fields.Many2many( + comodel_name="mrp.move", + relation="mrp_move_planned_order_rel", + column1="order_id", + column2="move_down_id", + string="MRP Move DOWN", + ) + mrp_action = fields.Selection( + selection=[("manufacture", "Manufacturing Order"), + ("buy", "Purchase Order"), + ("move", "Transfer"), + ("none", "None")], + string="Action", + ) + mrp_inventory_id = fields.Many2one( + string="Associated MRP Inventory", + comodel_name="mrp.inventory", + ondelete="set null", + ) diff --git a/mrp_multi_level/models/product_mrp_area.py b/mrp_multi_level/models/product_mrp_area.py index 682b4bd2e..508739ece 100644 --- a/mrp_multi_level/models/product_mrp_area.py +++ b/mrp_multi_level/models/product_mrp_area.py @@ -1,6 +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. +# - Jordi Ballester Alomar +# - Lois Rilo Antelo # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + from math import ceil from odoo import api, fields, models @@ -36,7 +39,7 @@ class ProductMRPArea(models.Model): mrp_minimum_order_qty = fields.Float( string='Minimum Order Qty', default=0.0, ) - mrp_minimum_stock = fields.Float(string='Minimum Stock') + mrp_minimum_stock = fields.Float(string="Safety Stock") mrp_nbr_days = fields.Integer( string='Nbr. Days', default=0, help="Number of days to group demand for this product during the " @@ -51,7 +54,7 @@ class ProductMRPArea(models.Model): ) mrp_lead_time = fields.Float( string='Lead Time', - related='product_id.produce_delay', + compute='_compute_mrp_lead_time', ) main_supplier_id = fields.Many2one( comodel_name='res.partner', string='Main Supplier', @@ -71,13 +74,20 @@ class ProductMRPArea(models.Model): compute='_compute_supply_method', ) - qty_available = fields.Float('Quantity Available', - compute='_compute_qty_available') + qty_available = fields.Float( + string="Quantity Available", + compute="_compute_qty_available", + ) mrp_move_ids = fields.One2many( comodel_name='mrp.move', inverse_name='product_mrp_area_id', readonly=True, ) + planned_order_ids = fields.One2many( + comodel_name="mrp.planned.order", + inverse_name="product_mrp_area_id", + readonly=True, + ) _sql_constraints = [ ('product_mrp_area_uniq', 'unique(product_id, mrp_area_id)', 'The product/MRP Area parameters combination must be unique.'), @@ -89,10 +99,21 @@ class ProductMRPArea(models.Model): area.mrp_area_id.name, area.product_id.display_name)) for area in self] + @api.multi + def _compute_mrp_lead_time(self): + produced = self.filtered(lambda r: r.supply_method == "manufacture") + purchased = self.filtered(lambda r: r.supply_method == "buy") + for rec in produced: + rec.mrp_lead_time = rec.product_id.produce_delay + for rec in purchased: + rec.mrp_lead_time = rec.main_supplierinfo_id.delay + # TODO: 'move' supply method. + for rec in (self - produced - purchased): + rec.mrp_lead_time = 0 + @api.multi def _compute_qty_available(self): for rec in self: - # TODO: move mrp_qty_available computation, maybe unreserved?? rec.qty_available = rec.product_id.with_context( {'location': rec.mrp_area_id.location_id.id}).qty_available diff --git a/mrp_multi_level/readme/DESCRIPTION.rst b/mrp_multi_level/readme/DESCRIPTION.rst index 65457aa0d..3da52486c 100644 --- a/mrp_multi_level/readme/DESCRIPTION.rst +++ b/mrp_multi_level/readme/DESCRIPTION.rst @@ -8,9 +8,9 @@ and explodes this down to the lowest level. Key Features ------------ -* MRP parameters at product variant level. +* MRP parameters set by product variant MRP area pairs. * Integration with `Stock Demand Estimates `_ system. * Cron job to calculate the MRP demand. * Manually calculate the MRP demand. * Confirm the calculated MRP demand and create PO's, or MO's. -* Able to see the products for which action is needed. +* Able to see the products for which action is needed throught Planned Orders. diff --git a/mrp_multi_level/readme/HISTORY.rst b/mrp_multi_level/readme/HISTORY.rst index 0b86704df..3b7f2db14 100644 --- a/mrp_multi_level/readme/HISTORY.rst +++ b/mrp_multi_level/readme/HISTORY.rst @@ -1,3 +1,9 @@ +11.0.3.0.0 (2019-05-22) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [REW/IMP] Rework to include Planned Orders. + (`#365 `_): + 11.0.2.2.0 (2019-05-02) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/mrp_multi_level/security/ir.model.access.csv b/mrp_multi_level/security/ir.model.access.csv index 953d83705..e709cbec1 100644 --- a/mrp_multi_level/security/ir.model.access.csv +++ b/mrp_multi_level/security/ir.model.access.csv @@ -7,3 +7,5 @@ access_mrp_area_user,mrp.area user,model_mrp_area,mrp.group_mrp_user,1,0,0,0 access_mrp_area_manager,mrp.area manager,model_mrp_area,mrp.group_mrp_manager,1,1,1,1 access_product_mrp_area_user,product.mrp.area user,model_product_mrp_area,mrp.group_mrp_user,1,1,1,0 access_product_mrp_area_manager,product.mrp.area manager,model_product_mrp_area,mrp.group_mrp_manager,1,1,1,1 +access_mrp_planned_order_user,mrp.planned.order user,model_mrp_planned_order,mrp.group_mrp_user,1,0,0,0 +access_mrp_planned_order_manager,mrp.planned.order manager,model_mrp_planned_order,mrp.group_mrp_manager,1,1,1,1 diff --git a/mrp_multi_level/static/description/index.html b/mrp_multi_level/static/description/index.html index aa8f60935..c95019ab8 100644 --- a/mrp_multi_level/static/description/index.html +++ b/mrp_multi_level/static/description/index.html @@ -376,51 +376,52 @@ and explodes this down to the lowest level.

Key Features

    -
  • MRP parameters at product variant level.
  • +
  • MRP parameters set by product variant MRP area pairs.
  • Integration with Stock Demand Estimates system.
  • Cron job to calculate the MRP demand.
  • Manually calculate the MRP demand.
  • Confirm the calculated MRP demand and create PO’s, or MO’s.
  • -
  • Able to see the products for which action is needed.
  • +
  • Able to see the products for which action is needed throught Planned Orders.

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.
  • @@ -428,7 +429,7 @@ the MRP parameters for a given product and area.
-

Usage

+

Usage

To manually run the MRP scheduler:

  1. Go to Manufacturing > Operations > Run MRP Multi Level.
  2. @@ -444,24 +445,31 @@ hand side gears in any record.
-

Changelog

+

Changelog

-

11.0.2.2.0 (2019-05-02)

+

11.0.3.0.0 (2019-05-22)

+
    +
  • [REW/IMP] Rework to include Planned Orders. +(#365):
  • +
+
+
+

11.0.2.2.0 (2019-05-02)

  • [IMP] Able to run MRP only for selected areas. (#360):
-
-

11.0.2.1.0 (2019-04-02)

+
+

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)

+
+

11.0.2.0.0 (2018-11-20)

  • [REW] Refactor MRP Area. (#322):
      @@ -473,15 +481,15 @@ different areas.
-
-

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)

  • [FIX] User and system locales doesn’t break MRP calculation. (#290)
  • @@ -490,15 +498,15 @@ as a related on MRP Areas. (#290)
-
-

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 @@ -506,16 +514,16 @@ If you spotted it first, help us smashing it by providing a detailed and welcome

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 09e00db97..1a43a1025 100644 --- a/mrp_multi_level/tests/test_mrp_multi_level.py +++ b/mrp_multi_level/tests/test_mrp_multi_level.py @@ -28,6 +28,7 @@ class TestMrpMultiLevel(SavepointCase): cls.mrp_inventory_procure_wiz = cls.env['mrp.inventory.procure'] cls.mrp_inventory_obj = cls.env['mrp.inventory'] cls.mrp_move_obj = cls.env['mrp.move'] + cls.planned_order_obj = cls.env['mrp.planned.order'] cls.fp_1 = cls.env.ref('mrp_multi_level.product_product_fp_1') cls.fp_2 = cls.env.ref('mrp_multi_level.product_product_fp_2') @@ -322,38 +323,36 @@ class TestMrpMultiLevel(SavepointCase): """Tests for mrp moves generated.""" moves = self.mrp_move_obj.search([ ('product_id', '=', self.pp_1.id), - ('mrp_action', '=', 'none'), ]) self.assertEqual(len(moves), 3) self.assertNotIn('s', moves.mapped('mrp_type')) for move in moves: - self.assertTrue(move.mrp_move_up_ids) - if move.mrp_move_up_ids.product_mrp_area_id.product_id == \ + self.assertTrue(move.planned_order_up_ids) + if move.planned_order_up_ids.product_mrp_area_id.product_id == \ self.fp_1: # Demand coming from FP-1 - self.assertEqual(move.mrp_move_up_ids.mrp_action, 'mo') + self.assertEqual( + move.planned_order_up_ids.mrp_action, "manufacture") self.assertEqual(move.mrp_qty, -200.0) - elif move.mrp_move_up_ids.product_mrp_area_id.product_id == \ + elif move.planned_order_up_ids.product_mrp_area_id.product_id == \ self.sf_1: # Demand coming from FP-2 -> SF-1 - self.assertEqual(move.mrp_move_up_ids.mrp_action, 'mo') + self.assertEqual( + move.planned_order_up_ids.mrp_action, "manufacture") if move.mrp_date == self.date_5: self.assertEqual(move.mrp_qty, -90.0) elif move.mrp_date == self.date_8: self.assertEqual(move.mrp_qty, -72.0) # Check actions: - moves = self.mrp_move_obj.search([ + planned_orders = self.planned_order_obj.search([ ('product_id', '=', self.pp_1.id), - ('mrp_action', '!=', 'none'), ]) - self.assertEqual(len(moves), 3) - for move in moves: - self.assertEqual(move.mrp_action, 'po') - self.assertEqual(move.mrp_type, 's') + self.assertEqual(len(planned_orders), 3) + for plan in planned_orders: + self.assertEqual(plan.mrp_action, 'buy') # Check PP-2 PO being accounted: po_move = self.mrp_move_obj.search([ ('product_id', '=', self.pp_2.id), - ('mrp_action', '=', 'none'), ('mrp_type', '=', 's'), ]) self.assertEqual(len(po_move), 1) @@ -452,16 +451,15 @@ class TestMrpMultiLevel(SavepointCase): self.assertEqual(pp_2_line_4.demand_qty, 48.0) self.assertEqual(pp_2_line_4.to_procure, 48.0) - def test_05_moves_extra_info(self): - """Test running availability and actions counters computation on - mrp moves.""" + def test_05_planned_availability(self): + """Test planned availability computation.""" # Running availability for PP-1: - moves = self.mrp_move_obj.search([ + invs = self.mrp_inventory_obj.search([ ('product_id', '=', self.pp_1.id)], - order='mrp_date, mrp_type desc, id') - self.assertEqual(len(moves), 6) - expected = [200.0, 290.0, 90.0, 0.0, 72.0, 0.0] - self.assertEqual(moves.mapped('running_availability'), expected) + order='date') + self.assertEqual(len(invs), 2) + expected = [0.0, 0.0] # No grouping, lot size nor safety stock. + self.assertEqual(invs.mapped('running_availability'), expected) def test_06_demand_estimates(self): """Tests demand estimates integration.""" @@ -480,8 +478,14 @@ class TestMrpMultiLevel(SavepointCase): self.assertIn(-30.0, quantities) # 210 a week => 30.0 dayly: self.assertIn(-40.0, quantities) # 280 a week => 40.0 dayly: 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) + plans = self.planned_order_obj.search([ + ('product_id', '=', self.prod_test.id), + ('mrp_area_id', '=', self.mrp_area.id), + ]) + action = list(set(plans.mapped("mrp_action"))) + self.assertEqual(len(action), 1) + self.assertEqual(action[0], "buy") + self.assertEqual(len(plans), 18) inventories = self.mrp_inventory_obj.search([ ('mrp_area_id', '=', self.secondary_area.id)]) self.assertEqual(len(inventories), 18) @@ -516,13 +520,12 @@ class TestMrpMultiLevel(SavepointCase): mrp_inv_max = self.mrp_inventory_obj.search([ ('product_mrp_area_id.product_id', '=', self.prod_max.id)]) self.assertEqual(mrp_inv_max.to_procure, 150) - moves = self.mrp_move_obj.search([ + plans = self.planned_order_obj.search([ ('product_id', '=', self.prod_max.id), - ('mrp_action', '!=', 'none'), ]) - self.assertEqual(len(moves), 2) - self.assertIn(100.0, moves.mapped('mrp_qty')) - self.assertIn(50.0, moves.mapped('mrp_qty')) + self.assertEqual(len(plans), 2) + self.assertIn(100.0, plans.mapped('mrp_qty')) + self.assertIn(50.0, plans.mapped('mrp_qty')) # quantity multiple: mrp_inv_multiple = self.mrp_inventory_obj.search([ ('product_mrp_area_id.product_id', '=', self.prod_multiple.id)]) @@ -538,13 +541,16 @@ class TestMrpMultiLevel(SavepointCase): ('product_id', '=', self.prod_test.id), ('mrp_area_id', '=', self.secondary_area.id), ]) + supply_plans = self.planned_order_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') + self.assertEqual(len(supply_plans), 3) + quantities = supply_plans.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')) diff --git a/mrp_multi_level/views/mrp_area_views.xml b/mrp_multi_level/views/mrp_area_views.xml index e887855ea..1478ff608 100644 --- a/mrp_multi_level/views/mrp_area_views.xml +++ b/mrp_multi_level/views/mrp_area_views.xml @@ -21,17 +21,28 @@ form

- - - - + +
+ +
+
+ diff --git a/mrp_multi_level/views/mrp_inventory_views.xml b/mrp_multi_level/views/mrp_inventory_views.xml index 0e804683e..e4a180ffb 100644 --- a/mrp_multi_level/views/mrp_inventory_views.xml +++ b/mrp_multi_level/views/mrp_inventory_views.xml @@ -1,7 +1,7 @@ - + mrp.inventory.form mrp.inventory form @@ -29,7 +29,7 @@ - + mrp.inventory.tree mrp.inventory tree @@ -49,6 +49,7 @@ name="%(mrp_multi_level.act_mrp_inventory_procure)d" icon="fa-cogs" type="action" attrs="{'invisible':[('to_procure','<=',0.0)]}"/> + @@ -78,7 +79,7 @@ - + mrp.inventory.search mrp.inventory search @@ -108,7 +109,7 @@ - + MRP Inventory mrp.inventory ir.actions.act_window diff --git a/mrp_multi_level/views/product_mrp_area_views.xml b/mrp_multi_level/views/product_mrp_area_views.xml index 11c8d02a0..5580953a4 100644 --- a/mrp_multi_level/views/product_mrp_area_views.xml +++ b/mrp_multi_level/views/product_mrp_area_views.xml @@ -49,13 +49,15 @@ - - + + + +
@@ -63,7 +65,6 @@ - @@ -73,12 +74,21 @@ - - - - - + + + + + + + + + + + + + + diff --git a/mrp_multi_level/wizards/mrp_inventory_procure.py b/mrp_multi_level/wizards/mrp_inventory_procure.py index 57fe8df8c..0bf65ca2c 100644 --- a/mrp_multi_level/wizards/mrp_inventory_procure.py +++ b/mrp_multi_level/wizards/mrp_inventory_procure.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,15 +17,16 @@ class MrpInventoryProcure(models.TransientModel): ) @api.model - def _prepare_item(self, mrp_inventory, qty_override=0.0): + def _prepare_item(self, planned_order): return { - 'qty': qty_override if qty_override else mrp_inventory.to_procure, - 'uom_id': mrp_inventory.uom_id.id, - 'date_planned': mrp_inventory.date, - 'mrp_inventory_id': mrp_inventory.id, - 'product_id': mrp_inventory.product_mrp_area_id.product_id.id, - 'warehouse_id': mrp_inventory.mrp_area_id.warehouse_id.id, - 'location_id': mrp_inventory.mrp_area_id.location_id.id, + 'planned_order_id': planned_order.id, + 'qty': planned_order.mrp_qty - planned_order.qty_released, + 'uom_id': planned_order.mrp_inventory_id.uom_id.id, + 'date_planned': planned_order.due_date, + 'mrp_inventory_id': planned_order.mrp_inventory_id.id, + 'product_id': planned_order.product_id.id, + 'warehouse_id': planned_order.mrp_area_id.warehouse_id.id, + 'location_id': planned_order.mrp_area_id.location_id.id, } @api.model @@ -56,17 +57,9 @@ class MrpInventoryProcure(models.TransientModel): assert active_model == 'mrp.inventory', 'Bad context propagation' items = item_obj = self.env['mrp.inventory.procure.item'] - for line in mrp_inventory_obj.browse(mrp_inventory_ids): - max_order = line.product_mrp_area_id.mrp_maximum_order_qty - qty_to_order = line.to_procure - if max_order and max_order < qty_to_order: - # split the procurement in batches: - while qty_to_order > 0.0: - qty = line.product_mrp_area_id._adjust_qty_to_order( - qty_to_order) - items += item_obj.create(self._prepare_item(line, qty)) - qty_to_order -= qty - else: + for line in mrp_inventory_obj.browse(mrp_inventory_ids).mapped( + 'planned_order_ids'): + if line.qty_released < line.mrp_qty: items += item_obj.create(self._prepare_item(line)) res['item_ids'] = [(6, 0, items.ids)] return res @@ -90,11 +83,9 @@ class MrpInventoryProcure(models.TransientModel): 'INT: ' + str(self.env.user.login), # origin? values ) - item.mrp_inventory_id.to_procure -= \ - item.uom_id._compute_quantity( - item.qty, item.product_id.uom_id) + item.planned_order_id.qty_released += item.qty except UserError as error: - errors.append(error.name) + errors.append(error.name) if errors: raise UserError('\n'.join(errors)) return {'type': 'ir.actions.act_window_close'} @@ -117,6 +108,9 @@ class MrpInventoryProcureItem(models.TransientModel): string='Mrp Inventory', comodel_name='mrp.inventory', ) + planned_order_id = fields.Many2one( + comodel_name='mrp.planned.order', + ) product_id = fields.Many2one( string='Product', comodel_name='product.product', diff --git a/mrp_multi_level/wizards/mrp_multi_level.py b/mrp_multi_level/wizards/mrp_multi_level.py index eccd80b8c..ec5204276 100644 --- a/mrp_multi_level/wizards/mrp_multi_level.py +++ b/mrp_multi_level/wizards/mrp_multi_level.py @@ -28,8 +28,7 @@ class MultiLevelMrp(models.TransientModel): qty_available = 0.0 product_obj = self.env['product.product'] # TODO: move mrp_qty_available computation, maybe unreserved?? - location_ids = self.env['stock.location'].search( - [('id', 'child_of', product_mrp_area.mrp_area_id.location_id.id)]) + location_ids = product_mrp_area.mrp_area_id._get_locations() for location in location_ids: product_l = product_obj.with_context( {'location': location.id}).browse( @@ -63,13 +62,10 @@ class MultiLevelMrp(models.TransientModel): 'current_qty': -daily_qty, 'mrp_date': date, 'current_date': date, - 'mrp_action': 'none', 'mrp_type': mrp_type, - 'mrp_processed': False, 'mrp_origin': origin, 'mrp_order_number': None, 'parent_product_id': None, - 'running_availability': 0.00, 'name': 'Forecast', 'state': 'confirmed', } @@ -127,39 +123,26 @@ class MultiLevelMrp(models.TransientModel): 'current_qty': product_qty, 'mrp_date': mrp_date, 'current_date': move.date_expected, - 'mrp_action': 'none', 'mrp_type': mrp_type, - 'mrp_processed': False, 'mrp_origin': origin, 'mrp_order_number': order_number, 'parent_product_id': parent_product_id, - 'running_availability': 0.00, 'name': order_number, 'state': move.state, } @api.model - def _prepare_mrp_move_data_supply( - self, product_mrp_area, qty, mrp_date_supply, mrp_action_date, - mrp_action, name): + def _prepare_planned_order_data( + self, product_mrp_area, qty, mrp_date_supply, + mrp_action_date, name + ): return { - 'product_id': product_mrp_area.product_id.id, 'product_mrp_area_id': product_mrp_area.id, - 'production_id': None, - 'purchase_order_id': None, - 'purchase_line_id': None, - 'stock_move_id': None, 'mrp_qty': qty, - 'current_qty': None, - 'mrp_date': mrp_date_supply, - 'mrp_action_date': mrp_action_date, - 'current_date': None, - 'mrp_action': mrp_action, - 'mrp_type': 's', - 'mrp_processed': False, - 'mrp_origin': None, - 'mrp_order_number': None, - 'parent_product_id': None, + 'due_date': mrp_date_supply, + 'order_release_date': mrp_action_date, + 'mrp_action': product_mrp_area.supply_method, + 'qty_released': 0.0, 'name': 'Supply: ' + name, } @@ -184,9 +167,7 @@ class MultiLevelMrp(models.TransientModel): 'current_qty': None, 'mrp_date': mrp_date_demand_2, 'current_date': None, - 'mrp_action': 'none', 'mrp_type': 'd', - 'mrp_processed': False, 'mrp_origin': 'mrp', 'mrp_order_number': None, 'parent_product_id': bom.product_id.id, @@ -198,82 +179,104 @@ class MultiLevelMrp(models.TransientModel): } @api.model - def create_move(self, product_mrp_area_id, mrp_date, mrp_qty, name): - self = self.with_context(auditlog_disabled=True) - - values = {} + def _get_action_and_supply_dates(self, product_mrp_area, mrp_date): if not isinstance(mrp_date, date): mrp_date = fields.Date.from_string(mrp_date) - if product_mrp_area_id.supply_method == 'buy': - # if product_mrp_area_id.purchase_requisition: - # mrp_action = 'pr' - # else: - mrp_action = 'po' - else: - # TODO: consider 'none'... - mrp_action = 'mo' - if mrp_date < date.today(): mrp_date_supply = date.today() else: mrp_date_supply = mrp_date - calendar = product_mrp_area_id.mrp_area_id.calendar_id - if calendar and product_mrp_area_id.mrp_lead_time: + calendar = product_mrp_area.mrp_area_id.calendar_id + if calendar and product_mrp_area.mrp_lead_time: date_str = fields.Date.to_string(mrp_date) dt = fields.Datetime.from_string(date_str) res = calendar.plan_days( - -1 * product_mrp_area_id.mrp_lead_time - 1, dt) + -1 * product_mrp_area.mrp_lead_time - 1, dt) mrp_action_date = res.date() else: mrp_action_date = mrp_date - timedelta( - days=product_mrp_area_id.mrp_lead_time) + days=product_mrp_area.mrp_lead_time) + return mrp_action_date, mrp_date_supply - qty_ordered = 0.00 + @api.model + def explode_action( + self, product_mrp_area_id, mrp_action_date, name, qty, action + ): + """Explode requirements.""" + mrp_date_demand = mrp_action_date + if mrp_date_demand < date.today(): + mrp_date_demand = date.today() + if not product_mrp_area_id.product_id.bom_ids: + return False + bomcount = 0 + for bom in product_mrp_area_id.product_id.bom_ids: + if not bom.active or not bom.bom_line_ids: + continue + bomcount += 1 + if bomcount != 1: + continue + for bomline in bom.bom_line_ids: + if bomline.product_qty <= 0.00 or \ + bomline.product_id.type != 'product': + continue + if self._exclude_from_mrp( + bomline.product_id, + product_mrp_area_id.mrp_area_id): + # Stop explosion. + continue + # TODO: review: mrp_transit_delay, mrp_inspection_delay + mrp_date_demand_2 = mrp_date_demand - timedelta( + days=(product_mrp_area_id.mrp_transit_delay + + product_mrp_area_id.mrp_inspection_delay)) + move_data = \ + self._prepare_mrp_move_data_bom_explosion( + product_mrp_area_id, bomline, qty, + mrp_date_demand_2, + bom, name) + mrpmove_id2 = self.env['mrp.move'].create(move_data) + if hasattr(action, "mrp_move_down_ids"): + action.mrp_move_down_ids = [(4, mrpmove_id2.id)] + return True + + @api.model + def create_action( + self, product_mrp_area_id, mrp_date, mrp_qty, name, values=None, + ): + if not values: + values = {} + if not isinstance(mrp_date, date): + mrp_date = fields.Date.from_string(mrp_date) + action_date, date_supply = \ + self._get_action_and_supply_dates(product_mrp_area_id, mrp_date) + return self.create_planned_order( + product_mrp_area_id, mrp_qty, name, date_supply, + action_date, values=values) + + @api.model + def create_planned_order( + self, product_mrp_area_id, mrp_qty, + name, mrp_date_supply, mrp_action_date, values=None, + ): + self = self.with_context(auditlog_disabled=True) + + qty_ordered = values.get("qty_ordered", 0.0) if values else 0.0 qty_to_order = mrp_qty while qty_ordered < mrp_qty: qty = product_mrp_area_id._adjust_qty_to_order(qty_to_order) qty_to_order -= qty - move_data = self._prepare_mrp_move_data_supply( + order_data = self._prepare_planned_order_data( product_mrp_area_id, qty, mrp_date_supply, mrp_action_date, - mrp_action, name) - mrpmove_id = self.env['mrp.move'].create(move_data) + name) + planned_order = self.env['mrp.planned.order'].create(order_data) qty_ordered = qty_ordered + qty - if mrp_action == 'mo': - mrp_date_demand = mrp_action_date - if mrp_date_demand < date.today(): - mrp_date_demand = date.today() - if not product_mrp_area_id.product_id.bom_ids: - continue - bomcount = 0 - for bom in product_mrp_area_id.product_id.bom_ids: - if not bom.active or not bom.bom_line_ids: - continue - bomcount += 1 - if bomcount != 1: - continue - for bomline in bom.bom_line_ids: - if bomline.product_qty <= 0.00 or \ - bomline.product_id.type != 'product': - continue - if self._exclude_from_mrp( - bomline.product_id, - product_mrp_area_id.mrp_area_id): - # Stop explosion. - continue - # TODO: review: mrp_transit_delay, mrp_inspection_delay - mrp_date_demand_2 = mrp_date_demand - timedelta( - days=(product_mrp_area_id.mrp_transit_delay + - product_mrp_area_id.mrp_inspection_delay)) - move_data = \ - self._prepare_mrp_move_data_bom_explosion( - product_mrp_area_id, bomline, qty, - mrp_date_demand_2, - bom, name) - mrpmove_id2 = self.env['mrp.move'].create(move_data) - mrpmove_id.mrp_move_down_ids = [(4, mrpmove_id2.id)] + if product_mrp_area_id.supply_method == 'manufacture': + self.explode_action( + product_mrp_area_id, mrp_action_date, + name, qty, planned_order) + values['qty_ordered'] = qty_ordered log_msg = '[%s] %s: qty_ordered = %s' % ( product_mrp_area_id.mrp_area_id.name, @@ -292,6 +295,8 @@ class MultiLevelMrp(models.TransientModel): domain += [('mrp_area_id', 'in', mrp_areas.ids)] self.env['mrp.move'].search(domain).unlink() self.env['mrp.inventory'].search(domain).unlink() + domain += [('fixed', '=', False)] + self.env['mrp.planned.order'].search(domain).unlink() logger.info('End MRP Cleanup') return True @@ -356,8 +361,7 @@ class MultiLevelMrp(models.TransientModel): @api.model def _init_mrp_move_from_forecast(self, product_mrp_area): - locations = self.env['stock.location'].search( - [('id', 'child_of', product_mrp_area.mrp_area_id.location_id.id)]) + locations = product_mrp_area.mrp_area_id._get_locations() today = fields.Date.today() estimates = self.env['stock.demand.estimate'].search([ ('product_id', '=', product_mrp_area.product_id.id), @@ -383,8 +387,7 @@ class MultiLevelMrp(models.TransientModel): # show moves with an action @api.model def _in_stock_moves_domain(self, product_mrp_area): - locations = self.env['stock.location'].search( - [('id', 'child_of', product_mrp_area.mrp_area_id.location_id.id)]) + locations = product_mrp_area.mrp_area_id._get_locations() return [ ('product_id', '=', product_mrp_area.product_id.id), ('state', 'not in', ['done', 'cancel']), @@ -395,8 +398,7 @@ class MultiLevelMrp(models.TransientModel): @api.model def _out_stock_moves_domain(self, product_mrp_area): - locations = self.env['stock.location'].search( - [('id', 'child_of', product_mrp_area.mrp_area_id.location_id.id)]) + locations = product_mrp_area.mrp_area_id._get_locations() return [ ('product_id', '=', product_mrp_area.product_id.id), ('state', 'not in', ['done', 'cancel']), @@ -443,21 +445,17 @@ class MultiLevelMrp(models.TransientModel): 'current_qty': poline.product_qty, 'mrp_date': mrp_date, 'current_date': poline.date_planned, - 'mrp_action': 'none', 'mrp_type': 's', - 'mrp_processed': False, 'mrp_origin': 'po', 'mrp_order_number': poline.order_id.name, 'parent_product_id': None, - 'running_availability': 0.00, 'name': poline.order_id.name, 'state': poline.order_id.state, } @api.model def _init_mrp_move_from_purchase_order(self, product_mrp_area): - location_ids = self.env['stock.location'].search( - [('id', 'child_of', product_mrp_area.mrp_area_id.location_id.id)]) + location_ids = product_mrp_area.mrp_area_id._get_locations() picking_types = self.env['stock.picking.type'].search( [('default_location_dest_id', 'in', location_ids.ids)]) @@ -528,8 +526,9 @@ class MultiLevelMrp(models.TransientModel): last_qty = 0.00 onhand = product_mrp_area.qty_available grouping_delta = product_mrp_area.mrp_nbr_days - for move in product_mrp_area.mrp_move_ids.filtered( - lambda m: m.mrp_action == 'none'): + for move in product_mrp_area.mrp_move_ids: + if self._exclude_move(move): + continue if last_date and ( fields.Date.from_string(move.mrp_date) >= last_date + timedelta(days=grouping_delta)) and ( @@ -539,7 +538,7 @@ class MultiLevelMrp(models.TransientModel): < 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( + cm = self.create_action( product_mrp_area_id=product_mrp_area, mrp_date=last_date, mrp_qty=qtytoorder, @@ -553,7 +552,7 @@ class MultiLevelMrp(models.TransientModel): product_mrp_area.mrp_minimum_stock or \ (onhand + last_qty) < \ product_mrp_area.mrp_minimum_stock: - if not last_date: + if not last_date or last_qty == 0.0: last_date = fields.Date.from_string(move.mrp_date) last_qty = move.mrp_qty else: @@ -565,7 +564,7 @@ class MultiLevelMrp(models.TransientModel): 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( + cm = self.create_action( product_mrp_area_id=product_mrp_area, mrp_date=last_date, mrp_qty=qtytoorder, name=name) qty_ordered = cm.get('qty_ordered', 0.0) @@ -573,6 +572,11 @@ class MultiLevelMrp(models.TransientModel): nbr_create += 1 return nbr_create + @api.model + def _exclude_move(self, move): + """Improve extensibility being able to exclude special moves.""" + return False + @api.model def _mrp_calculation(self, mrp_lowest_llc, mrp_areas): logger.info('Start MRP calculation') @@ -591,25 +595,22 @@ class MultiLevelMrp(models.TransientModel): for product_mrp_area in product_mrp_areas: nbr_create = 0 onhand = product_mrp_area.qty_available - # TODO: unreserved? if product_mrp_area.mrp_nbr_days == 0: - # todo: review ordering by date for move in product_mrp_area.mrp_move_ids: - if move.mrp_action == 'none': - if (onhand + move.mrp_qty) < \ - product_mrp_area.mrp_minimum_stock: - qtytoorder = \ - product_mrp_area.mrp_minimum_stock - \ - onhand - move.mrp_qty - cm = self.create_move( - product_mrp_area_id=product_mrp_area, - mrp_date=move.mrp_date, - mrp_qty=qtytoorder, name=move.name) - qty_ordered = cm['qty_ordered'] - onhand += move.mrp_qty + qty_ordered - nbr_create += 1 - else: - onhand += move.mrp_qty + if self._exclude_move(move): + continue + qtytoorder = product_mrp_area.mrp_minimum_stock - \ + onhand - move.mrp_qty + if qtytoorder > 0.0: + cm = self.create_action( + product_mrp_area_id=product_mrp_area, + mrp_date=move.mrp_date, + mrp_qty=qtytoorder, name=move.name) + qty_ordered = cm['qty_ordered'] + onhand += move.mrp_qty + qty_ordered + nbr_create += 1 + else: + onhand += move.mrp_qty else: nbr_create = self._init_mrp_move_grouped_demand( nbr_create, product_mrp_area) @@ -618,7 +619,7 @@ class MultiLevelMrp(models.TransientModel): nbr_create == 0: qtytoorder = \ product_mrp_area.mrp_minimum_stock - onhand - cm = self.create_move( + cm = self.create_action( product_mrp_area_id=product_mrp_area, mrp_date=date.today(), mrp_qty=qtytoorder, @@ -654,35 +655,30 @@ class MultiLevelMrp(models.TransientModel): FROM mrp_move WHERE product_mrp_area_id = %(mrp_product)s AND mrp_type = 's' - AND mrp_action = 'none' GROUP BY mrp_date """ + params = { + 'mrp_product': product_mrp_area.id, + } + return query, params + + @api.model + def _get_planned_order_groups(self, product_mrp_area): + query = """ + SELECT due_date, sum(mrp_qty) + FROM mrp_planned_order + WHERE product_mrp_area_id = %(mrp_product)s + GROUP BY due_date + """ params = { 'mrp_product': product_mrp_area.id } return query, params - @api.model - def _get_supply_action_groups(self, product_mrp_area): - exclude_mrp_actions = ['none', 'cancel'] - query = """ - SELECT mrp_date, sum(mrp_qty) - FROM mrp_move - WHERE product_mrp_area_id = %(mrp_product)s - AND mrp_qty <> 0.0 - AND mrp_type = 's' - AND mrp_action not in %(excluded_mrp_actions)s - GROUP BY mrp_date - """ - params = { - 'mrp_product': product_mrp_area.id, - 'excluded_mrp_actions': tuple(exclude_mrp_actions,) - } - return query, params - @api.model def _init_mrp_inventory(self, product_mrp_area): mrp_move_obj = self.env['mrp.move'] + planned_order_obj = self.env['mrp.planned.order'] # Read Demand demand_qty_by_date = {} query, params = self._get_demand_groups(product_mrp_area) @@ -695,44 +691,49 @@ class MultiLevelMrp(models.TransientModel): self.env.cr.execute(query, params) for mrp_date, qty in self.env.cr.fetchall(): supply_qty_by_date[mrp_date] = qty - # Read supply actions - # TODO: if we remove cancel take it into account here, - # TODO: as well as mrp_type ('r'). - supply_actions_qty_by_date = {} - query, params = self._get_supply_action_groups(product_mrp_area) + # Read planned orders: + planned_qty_by_date = {} + query, params = self._get_planned_order_groups(product_mrp_area) self.env.cr.execute(query, params) for mrp_date, qty in self.env.cr.fetchall(): - supply_actions_qty_by_date[mrp_date] = qty + planned_qty_by_date[mrp_date] = qty # Dates - mrp_dates = set(mrp_move_obj.search([ + moves_dates = mrp_move_obj.search([ ('product_mrp_area_id', '=', product_mrp_area.id)], - order='mrp_date').mapped('mrp_date')) + order='mrp_date').mapped('mrp_date') + action_dates = planned_order_obj.search([ + ('product_mrp_area_id', '=', product_mrp_area.id)], + order='due_date').mapped('due_date') + mrp_dates = set(moves_dates + action_dates) on_hand_qty = product_mrp_area.product_id.with_context( location=product_mrp_area.mrp_area_id.location_id.id )._product_available()[ product_mrp_area.product_id.id]['qty_available'] - # TODO: unreserved? + running_availability = on_hand_qty for mdt in sorted(mrp_dates): mrp_inventory_data = { 'product_mrp_area_id': product_mrp_area.id, 'date': mdt, } - demand_qty = 0.0 - supply_qty = 0.0 - if mdt in demand_qty_by_date.keys(): - demand_qty = demand_qty_by_date[mdt] - mrp_inventory_data['demand_qty'] = abs(demand_qty) - if mdt in supply_qty_by_date.keys(): - supply_qty = supply_qty_by_date[mdt] - mrp_inventory_data['supply_qty'] = abs(supply_qty) - if mdt in supply_actions_qty_by_date.keys(): - mrp_inventory_data['to_procure'] = \ - supply_actions_qty_by_date[mdt] + demand_qty = demand_qty_by_date.get(mdt, 0.0) + mrp_inventory_data['demand_qty'] = abs(demand_qty) + supply_qty = supply_qty_by_date.get(mdt, 0.0) + mrp_inventory_data['supply_qty'] = abs(supply_qty) mrp_inventory_data['initial_on_hand_qty'] = on_hand_qty on_hand_qty += (supply_qty + demand_qty) mrp_inventory_data['final_on_hand_qty'] = on_hand_qty + # Consider that MRP plan is followed exactly: + running_availability += supply_qty \ + + demand_qty + planned_qty_by_date.get(mdt, 0.0) + mrp_inventory_data['running_availability'] = running_availability - self.env['mrp.inventory'].create(mrp_inventory_data) + inv_id = self.env['mrp.inventory'].create(mrp_inventory_data) + # attach planned orders to inventory + planned_order_obj.search([ + ('due_date', '=', mdt), + ('product_mrp_area_id', '=', product_mrp_area.id), + ]).write( + {'mrp_inventory_id': inv_id.id}) @api.model def _mrp_final_process(self, mrp_areas): @@ -745,16 +746,6 @@ class MultiLevelMrp(models.TransientModel): for product_mrp_area in product_mrp_area_ids: # Build the time-phased inventory self._init_mrp_inventory(product_mrp_area) - - # Complete info on mrp_move (running availability and nbr actions) - qoh = product_mrp_area.qty_available - - moves = self.env['mrp.move'].search([ - ('product_mrp_area_id', '=', product_mrp_area.id)], - order='mrp_date, mrp_type desc, id') - for move in moves: - qoh = qoh + move.mrp_qty - move.running_availability = qoh logger.info('End MRP final process') @api.multi