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 1333ce9c9..62a282afc 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,6 @@ def _search_open_stock_moves_domain(self): "in", ["draft", "waiting", "confirmed", "partially_available", "assigned"], ), - ("location_dest_id", "=", self.location_id.id), ] @api.model @@ -1369,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 @@ -1384,13 +1394,15 @@ def _get_dates_adu_past_demand(self, horizon): ) return date_from, date_to - def _past_demand_estimate_domain(self, date_from, date_to, locations): + def _past_mrp_move_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), + ("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_moves_domain(self, date_from, date_to, locations): @@ -1419,24 +1431,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 - domain = self._past_demand_estimate_domain(date_from, date_to, locations) + 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._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) ) - 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,13 +1463,15 @@ def _get_dates_adu_future_demand(self, horizon): ) return date_from, date_to - def _future_demand_estimate_domain(self, date_from, date_to, locations): + def _future_mrp_move_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), + ("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_moves_domain(self, date_from, date_to, locations): @@ -1482,24 +1496,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 - domain = self._future_demand_estimate_domain(date_from, date_to, locations) + 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._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) ) - 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 +1885,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( @@ -1886,7 +1900,7 @@ def action_view_past_adu(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" @@ -1895,7 +1909,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( @@ -1910,7 +1939,7 @@ def action_view_future_adu(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" @@ -1919,6 +1948,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..b65beae0f 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 = (datetime.today() - timedelta(days=1)).date() + 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 @@