From 038b5d7526f0d7758107d5aa4f6c6a3fde4ea19e Mon Sep 17 00:00:00 2001 From: BernatPForgeFlow Date: Tue, 17 Oct 2023 17:30:38 +0200 Subject: [PATCH 1/3] [IMP] ddmrp: Add new ADU calculation method Allow the calculation of ADU from Direct demand (Stock Demand Estimates) and Indirect (MRP Moves) --- .../product_adu_calculation_method_data.xml | 9 ++ .../models/product_adu_calculation_method.py | 1 + ddmrp/models/stock_buffer.py | 84 ++++++++++++---- ddmrp/tests/test_ddmrp.py | 95 +++++++++++++++++++ ddmrp/views/stock_buffer_view.xml | 22 ++++- 5 files changed, 191 insertions(+), 20 deletions(-) diff --git a/ddmrp/data/product_adu_calculation_method_data.xml b/ddmrp/data/product_adu_calculation_method_data.xml index 1e305df7f..bdd2dc922 100644 --- a/ddmrp/data/product_adu_calculation_method_data.xml +++ b/ddmrp/data/product_adu_calculation_method_data.xml @@ -19,4 +19,13 @@ estimates 120 + + Future (Direct + Indirect)(120 days) + future + estimates_mrp + 120 + diff --git a/ddmrp/models/product_adu_calculation_method.py b/ddmrp/models/product_adu_calculation_method.py index a05d7bbe5..2be2ac997 100644 --- a/ddmrp/models/product_adu_calculation_method.py +++ b/ddmrp/models/product_adu_calculation_method.py @@ -24,6 +24,7 @@ def _get_source_selection(self): return [ ("actual", "Use actual Stock Moves"), ("estimates", "Use Demand Estimates"), + ("estimates_mrp", "Use Demand Estimates + Indirect Demand from MRP Moves"), ] name = fields.Char(string="Name", required=True) diff --git a/ddmrp/models/stock_buffer.py b/ddmrp/models/stock_buffer.py index d71cfa45a..8b24ec8fe 100644 --- a/ddmrp/models/stock_buffer.py +++ b/ddmrp/models/stock_buffer.py @@ -1384,6 +1384,17 @@ def _get_dates_adu_past_demand(self, horizon): ) return date_from, date_to + def _past_mrp_move_domain(self, date_from, date_to, locations): + self.ensure_one() + return [ + ("product_id", "=", self.product_id.id), + ("mrp_date", "<=", date_to), + ("mrp_date", ">=", date_from), + ("mrp_area_id.location_id", "in", locations.ids), + ("mrp_type", "=", "d"), + ("mrp_origin", "in", ["mrp", "mo"]), + ] + def _past_demand_estimate_domain(self, date_from, date_to, locations): self.ensure_one() return [ @@ -1419,24 +1430,24 @@ def _calc_adu_past_demand(self): .with_context(active_test=False) .search([("id", "child_of", self.location_id.ids)]) ) - if self.adu_calculation_method.source_past == "estimates": - qty = 0.0 + qty = 0.0 + if self.adu_calculation_method.source_past == "estimates_mrp": + domain = self._past_mrp_move_domain(date_from, date_to, locations) + for mrp_move in self.env["mrp.move"].search(domain): + qty += -mrp_move.mrp_qty + if self.adu_calculation_method.source_past in ["estimates", "estimates_mrp"]: domain = self._past_demand_estimate_domain(date_from, date_to, locations) for estimate in self.env["stock.demand.estimate"].search(domain): qty += estimate.get_quantity_by_date_range( fields.Date.from_string(date_from), fields.Date.from_string(date_to) ) - return qty / horizon elif self.adu_calculation_method.source_past == "actual": - qty = 0.0 domain = self._past_moves_domain(date_from, date_to, locations) for group in self.env["stock.move"].read_group( domain, ["product_id", "product_qty"], ["product_id"] ): qty += group["product_qty"] - return qty / horizon - else: - return 0.0 + return qty / horizon def _get_horizon_adu_future_demand(self): return self.adu_calculation_method.horizon_future or 1 @@ -1451,6 +1462,17 @@ def _get_dates_adu_future_demand(self, horizon): ) return date_from, date_to + def _future_mrp_move_domain(self, date_from, date_to, locations): + self.ensure_one() + return [ + ("product_id", "=", self.product_id.id), + ("mrp_date", "<=", date_to), + ("mrp_date", ">=", date_from), + ("mrp_area_id.location_id", "in", locations.ids), + ("mrp_type", "=", "d"), + ("mrp_origin", "in", ["mrp", "mo"]), + ] + def _future_demand_estimate_domain(self, date_from, date_to, locations): self.ensure_one() return [ @@ -1482,24 +1504,24 @@ def _calc_adu_future_demand(self): locations = self.env["stock.location"].search( [("id", "child_of", [self.location_id.id])] ) - if self.adu_calculation_method.source_future == "estimates": - qty = 0.0 + qty = 0.0 + if self.adu_calculation_method.source_future == "estimates_mrp": + domain = self._future_mrp_move_domain(date_from, date_to, locations) + for mrp_move in self.env["mrp.move"].search(domain): + qty += -mrp_move.mrp_qty + if self.adu_calculation_method.source_future in ["estimates", "estimates_mrp"]: domain = self._future_demand_estimate_domain(date_from, date_to, locations) for estimate in self.env["stock.demand.estimate"].search(domain): qty += estimate.get_quantity_by_date_range( fields.Date.from_string(date_from), fields.Date.from_string(date_to) ) - return qty / horizon elif self.adu_calculation_method.source_future == "actual": - qty = 0.0 domain = self._future_moves_domain(date_from, date_to, locations) for group in self.env["stock.move"].read_group( domain, ["product_id", "product_qty"], ["product_id"] ): qty += group["product_qty"] - return qty / horizon - else: - return 0.0 + return qty / horizon def _calc_adu_blended(self): self.ensure_one() @@ -1871,7 +1893,7 @@ def action_view_qualified_demand_mrp(self): result["domain"] = [("id", "in", mrp_moves.ids)] return result - def action_view_past_adu(self): + def action_view_past_adu_direct_demand(self): horizon = self._get_horizon_adu_past_demand() date_from, date_to = self._get_dates_adu_past_demand(horizon) locations = self.env["stock.location"].search( @@ -1895,7 +1917,22 @@ def action_view_past_adu(self): result["domain"] = [("id", "in", estimates.ids)] return result - def action_view_future_adu(self): + def action_view_past_adu_indirect_demand(self): + horizon = self._get_horizon_adu_past_demand() + date_from, date_to = self._get_dates_adu_past_demand(horizon) + locations = self.env["stock.location"].search( + [("id", "child_of", [self.location_id.id])] + ) + domain = self._past_mrp_move_domain(date_from, date_to, locations) + mrp_moves = self.env["mrp.move"].search(domain) + result = self.env["ir.actions.actions"]._for_xml_id( + "mrp_multi_level.mrp_move_action" + ) + result["context"] = {} + result["domain"] = [("id", "in", mrp_moves.ids)] + return result + + def action_view_future_adu_direct_demand(self): horizon = self._get_horizon_adu_future_demand() date_from, date_to = self._get_dates_adu_future_demand(horizon) locations = self.env["stock.location"].search( @@ -1919,6 +1956,21 @@ def action_view_future_adu(self): result["domain"] = [("id", "in", estimates.ids)] return result + def action_view_future_adu_indirect_demand(self): + horizon = self._get_horizon_adu_future_demand() + date_from, date_to = self._get_dates_adu_future_demand(horizon) + locations = self.env["stock.location"].search( + [("id", "child_of", [self.location_id.id])] + ) + domain = self._future_mrp_move_domain(date_from, date_to, locations) + mrp_moves = self.env["mrp.move"].search(domain) + result = self.env["ir.actions.actions"]._for_xml_id( + "mrp_multi_level.mrp_move_action" + ) + result["context"] = {} + result["domain"] = [("id", "in", mrp_moves.ids)] + return result + @api.model def cron_ddmrp_adu(self, automatic=False): """calculate ADU for each DDMRP buffer. Called by cronjob.""" diff --git a/ddmrp/tests/test_ddmrp.py b/ddmrp/tests/test_ddmrp.py index f3c2753cf..ad3f5f7ff 100644 --- a/ddmrp/tests/test_ddmrp.py +++ b/ddmrp/tests/test_ddmrp.py @@ -1104,3 +1104,98 @@ def test_44_resupply_from_another_warehouse(self): buffer_distributed.distributed_source_location_id, self.warehouse.lot_stock_id, ) + + def test_45_adu_calculation_blended_120_days_estimated_mrp(self): + """Test blended ADU calculation method with direct and indirect demand.""" + mrpMoveModel = self.env["mrp.move"] + mrpAreaModel = self.env["mrp.area"] + productMrpAreaModel = self.env["product.mrp.area"] + method = self.aducalcmethodModel.create( + { + "name": "Blended (120 d. estimates_mrp past, 120 d. estimates_mrp future)", + "method": "blended", + "source_past": "estimates_mrp", + "horizon_past": 120, + "factor_past": 0.5, + "source_future": "estimates_mrp", + "horizon_future": 120, + "factor_future": 0.5, + "company_id": self.main_company.id, + } + ) + self.buffer_a.adu_calculation_method = method.id + mrp_area_id = mrpAreaModel.create( + { + "name": "WH/Stock", + "warehouse_id": self.warehouse.id, + "location_id": self.stock_location.id, + } + ) + product_mrp_area_id = productMrpAreaModel.create( + { + "mrp_area_id": mrp_area_id.id, + "product_id": self.productA.id, + } + ) + today = fields.Date.today() + + # Past. + # create estimate: 120 units / 120 days = 1 unit/day + # create mrp move: 120 units / 120 days = 1 unit/day + dt = self.calendar.plan_days(-1 * 120, datetime.today()) + estimate_date_from = dt.date() + estimate_date_to = self.estimate_date_from - timedelta(days=1) + self.estimateModel.create( + { + "manual_date_from": estimate_date_from, + "manual_date_to": estimate_date_to, + "product_id": self.productA.id, + "product_uom_qty": 120, + "product_uom": self.productA.uom_id.id, + "location_id": self.stock_location.id, + } + ) + mrpMoveModel.create( + { + "mrp_area_id": product_mrp_area_id.mrp_area_id.id, + "product_id": product_mrp_area_id.product_id.id, + "product_mrp_area_id": product_mrp_area_id.id, + "mrp_qty": -120, + "current_qty": 0, + "mrp_date": today - timedelta(days=5), + "current_date": None, + "mrp_type": "d", + "mrp_origin": "mrp", + } + ) + + # Future. + # create estimate: 120 units / 120 days = 1 unit/day + # create mrp move: 120 units / 120 days = 1 unit/day + self.estimateModel.create( + { + "manual_date_from": self.estimate_date_from, + "manual_date_to": self.estimate_date_to, + "product_id": self.productA.id, + "product_uom_qty": 120, + "product_uom": self.productA.uom_id.id, + "location_id": self.stock_location.id, + } + ) + mrpMoveModel.create( + { + "mrp_area_id": product_mrp_area_id.mrp_area_id.id, + "product_id": product_mrp_area_id.product_id.id, + "product_mrp_area_id": product_mrp_area_id.id, + "mrp_qty": -120, + "current_qty": 0, + "mrp_date": today + timedelta(days=5), + "current_date": None, + "mrp_type": "d", + "mrp_origin": "mrp", + } + ) + + self.bufferModel.cron_ddmrp_adu() + to_assert_value = 2 * 0.5 + 2 * 0.5 + self.assertEqual(self.buffer_a.adu, to_assert_value) diff --git a/ddmrp/views/stock_buffer_view.xml b/ddmrp/views/stock_buffer_view.xml index 7b068619b..a95f59cac 100644 --- a/ddmrp/views/stock_buffer_view.xml +++ b/ddmrp/views/stock_buffer_view.xml @@ -230,19 +230,33 @@
Date: Tue, 31 Oct 2023 16:29:13 +0100 Subject: [PATCH 2/3] [IMP] ddmrp: Unify '_demand_estimate_domain' in one method --- ddmrp/models/stock_buffer.py | 48 ++++++++++++++---------------------- ddmrp/tests/test_ddmrp.py | 2 +- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/ddmrp/models/stock_buffer.py b/ddmrp/models/stock_buffer.py index 8b24ec8fe..b5b40d923 100644 --- a/ddmrp/models/stock_buffer.py +++ b/ddmrp/models/stock_buffer.py @@ -200,6 +200,18 @@ def action_view_yearly_consumption(self): action["domain"] = self._past_moves_domain(date_from, date_to, locations) return action + def _demand_estimate_domain(self, locations, date_from=False, date_to=False): + self.ensure_one() + domain = [ + ("location_id", "in", locations.ids), + ("product_id", "=", self.product_id.id), + ] + if date_to: + domain += [("date_from", "<=", date_to)] + if date_from: + domain += [("date_to", ">=", date_from)] + return domain + def action_view_stock_demand_estimates(self): result = self.env["ir.actions.actions"]._for_xml_id( "stock_demand_estimate.stock_demand_estimate_action" @@ -207,12 +219,8 @@ def action_view_stock_demand_estimates(self): locations = self.env["stock.location"].search( [("id", "child_of", [self.location_id.id])] ) - recs = self.env["stock.demand.estimate"].search( - [ - ("product_id", "=", self.product_id.id), - ("location_id", "in", locations.ids), - ] - ) + domain = self._demand_estimate_domain(locations) + recs = self.env["stock.demand.estimate"].search(domain) result["domain"] = [("id", "in", recs.ids)] return result @@ -1341,7 +1349,7 @@ def _search_open_stock_moves_domain(self): "in", ["draft", "waiting", "confirmed", "partially_available", "assigned"], ), - ("location_dest_id", "=", self.location_id.id), + ("location_dest_id", "child_of", [self.location_id.id]), ] @api.model @@ -1395,15 +1403,6 @@ def _past_mrp_move_domain(self, date_from, date_to, locations): ("mrp_origin", "in", ["mrp", "mo"]), ] - def _past_demand_estimate_domain(self, date_from, date_to, locations): - self.ensure_one() - return [ - ("location_id", "in", locations.ids), - ("product_id", "=", self.product_id.id), - ("date_from", "<=", date_to), - ("date_to", ">=", date_from), - ] - def _past_moves_domain(self, date_from, date_to, locations): self.ensure_one() domain = [ @@ -1436,7 +1435,7 @@ def _calc_adu_past_demand(self): for mrp_move in self.env["mrp.move"].search(domain): qty += -mrp_move.mrp_qty if self.adu_calculation_method.source_past in ["estimates", "estimates_mrp"]: - domain = self._past_demand_estimate_domain(date_from, date_to, locations) + domain = self._demand_estimate_domain(locations, date_from, date_to) for estimate in self.env["stock.demand.estimate"].search(domain): qty += estimate.get_quantity_by_date_range( fields.Date.from_string(date_from), fields.Date.from_string(date_to) @@ -1473,15 +1472,6 @@ def _future_mrp_move_domain(self, date_from, date_to, locations): ("mrp_origin", "in", ["mrp", "mo"]), ] - def _future_demand_estimate_domain(self, date_from, date_to, locations): - self.ensure_one() - return [ - ("location_id", "in", locations.ids), - ("product_id", "=", self.product_id.id), - ("date_from", "<=", date_to), - ("date_to", ">=", date_from), - ] - def _future_moves_domain(self, date_from, date_to, locations): self.ensure_one() domain = [ @@ -1510,7 +1500,7 @@ def _calc_adu_future_demand(self): for mrp_move in self.env["mrp.move"].search(domain): qty += -mrp_move.mrp_qty if self.adu_calculation_method.source_future in ["estimates", "estimates_mrp"]: - domain = self._future_demand_estimate_domain(date_from, date_to, locations) + domain = self._demand_estimate_domain(locations, date_from, date_to) for estimate in self.env["stock.demand.estimate"].search(domain): qty += estimate.get_quantity_by_date_range( fields.Date.from_string(date_from), fields.Date.from_string(date_to) @@ -1908,7 +1898,7 @@ def action_view_past_adu_direct_demand(self): result["context"] = {} result["domain"] = [("id", "in", moves.ids)] else: - domain = self._past_demand_estimate_domain(date_from, date_to, locations) + domain = self._demand_estimate_domain(locations, date_from, date_to) estimates = self.env["stock.demand.estimate"].search(domain) result = self.env["ir.actions.actions"]._for_xml_id( "stock_demand_estimate.stock_demand_estimate_action" @@ -1947,7 +1937,7 @@ def action_view_future_adu_direct_demand(self): result["context"] = {} result["domain"] = [("id", "in", moves.ids)] else: - domain = self._future_demand_estimate_domain(date_from, date_to, locations) + domain = self._demand_estimate_domain(locations, date_from, date_to) estimates = self.env["stock.demand.estimate"].search(domain) result = self.env["ir.actions.actions"]._for_xml_id( "stock_demand_estimate.stock_demand_estimate_action" diff --git a/ddmrp/tests/test_ddmrp.py b/ddmrp/tests/test_ddmrp.py index ad3f5f7ff..b65beae0f 100644 --- a/ddmrp/tests/test_ddmrp.py +++ b/ddmrp/tests/test_ddmrp.py @@ -1144,7 +1144,7 @@ def test_45_adu_calculation_blended_120_days_estimated_mrp(self): # create mrp move: 120 units / 120 days = 1 unit/day dt = self.calendar.plan_days(-1 * 120, datetime.today()) estimate_date_from = dt.date() - estimate_date_to = self.estimate_date_from - timedelta(days=1) + estimate_date_to = (datetime.today() - timedelta(days=1)).date() self.estimateModel.create( { "manual_date_from": estimate_date_from, From 25427323665eeefae987585eaeafbea6b1fe0036 Mon Sep 17 00:00:00 2001 From: BernatPForgeFlow Date: Thu, 16 Nov 2023 12:05:52 +0100 Subject: [PATCH 3/3] [IMP] ddmrp: Optimize open_moves search --- ddmrp/models/stock_buffer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ddmrp/models/stock_buffer.py b/ddmrp/models/stock_buffer.py index b5b40d923..7d047cc29 100644 --- a/ddmrp/models/stock_buffer.py +++ b/ddmrp/models/stock_buffer.py @@ -1349,7 +1349,6 @@ def _search_open_stock_moves_domain(self): "in", ["draft", "waiting", "confirmed", "partially_available", "assigned"], ), - ("location_dest_id", "child_of", [self.location_id.id]), ] @api.model @@ -1377,8 +1376,11 @@ def open_moves(self): # Utility method used to add an "Open Moves" button in the buffer # planning view domain = self._search_open_stock_moves_domain() - records = self.env["stock.move"].search(domain) - return self._stock_move_tree_view(records) + moves = self.env["stock.move"].search(domain) + moves = moves.filtered( + lambda move: move.location_dest_id.is_sublocation_of(self.location_id) + ) + return self._stock_move_tree_view(moves) def _get_horizon_adu_past_demand(self): return self.adu_calculation_method.horizon_past or 0