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 @@
+
+