first commit
This commit is contained in:
commit
e094a61cd7
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
__pycache__/
|
||||||
|
*.pot
|
||||||
|
*.po
|
||||||
|
|
||||||
|
# Odoo specific
|
||||||
|
/.idea
|
||||||
|
/.vscode
|
||||||
|
.DS_Store
|
||||||
30
README.md
Normal file
30
README.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# MPB Bottom-Up Replenishment (Two-Way MPS)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This module (`mrp_mps_bottom_up`) enhances the standard Odoo 19 Master Production Schedule (MPS) by introducing a "two-way" propagation of manual replenishment quantity changes.
|
||||||
|
|
||||||
|
In standard Odoo MPS, modifying the replenishment quantity of a top-level component (`FG1`) creates downward indirect demand for its sub-components (`FG` or `WIP`), but modifying a sub-component (`FG`) has no effect on its parent (`FG1`).
|
||||||
|
|
||||||
|
This module intercepts manual edits to the `replenish_qty` cell and automatically:
|
||||||
|
1. **Traces Up (Bottom-Up):** Calculates the equivalent base quantity required for any parent products and seamlessly updates their replenishment quantities in the same period without triggering infinite loops.
|
||||||
|
2. **Drills Down (Top-Down):** Calculates the equivalent quantity required for child components and explicitly pushes this demand down.
|
||||||
|
|
||||||
|
It fully supports sub-component scaling based on correct BOM ratios.
|
||||||
|
It includes built-in compatibility with custom packaging quantities (e.g. from the `mrp_packaging_qty` module), accurately multiplying or dividing base units by packaging multipliers during the Drill Down operation.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Add the module to your custom addons directory and install it via the regular Odoo `Apps` menu. The module implicitly depends on the core `mrp_mps` enterprise module.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Open the **Master Production Schedule** view.
|
||||||
|
2. Ensure you have product hierarchies defined with BOMs (e.g. `Product A` requires 2x `Product B`).
|
||||||
|
3. Add both `Product A` and `Product B` to your MPS view.
|
||||||
|
4. If you artificially increase the Replenishment for the child `Product B`, you will see `Product A` automatically adjust to reflect the scaled parent demand.
|
||||||
|
5. If you increase the Replenishment for a middle component, you will observe the quantity propagate up to parents AND down to children simultaneously.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
* `mrp_mps` (Core Odoo Enterprise module)
|
||||||
|
* Compatible with `mrp_packaging_qty` (Custom Module)
|
||||||
|
|
||||||
|
## License
|
||||||
|
Licensed under LGPL-3.0.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
20
__manifest__.py
Normal file
20
__manifest__.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': "MPB Bottom-Up Replenishment (Two-Way MPS)",
|
||||||
|
'summary': "Allow bottom-up & top-down calculation in Master Production Schedule",
|
||||||
|
'description': """
|
||||||
|
This module overrides the MPS replenishment behavior.
|
||||||
|
Normally, setting a replenishment value in MPS only creates indirect demand downwards.
|
||||||
|
This module adds a trace-up (bottom-up) functionality to calculate the equivalent
|
||||||
|
replenishment quantities for parent products, and a drill-down (top-down) functionality
|
||||||
|
to calculate the equivalent replenishment quantities for child components.
|
||||||
|
""",
|
||||||
|
'author': "Suherdy",
|
||||||
|
'category': 'Manufacturing/Manufacturing',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'depends': ['mrp_mps'],
|
||||||
|
'data': [],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
1
models/__init__.py
Normal file
1
models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import mrp_mps
|
||||||
102
models/mrp_mps.py
Normal file
102
models/mrp_mps.py
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
from odoo import models, api
|
||||||
|
from odoo.tools.float_utils import float_round
|
||||||
|
|
||||||
|
class MrpProductionSchedule(models.Model):
|
||||||
|
_inherit = 'mrp.production.schedule'
|
||||||
|
|
||||||
|
def set_replenish_qty(self, date_index, quantity, period_scale=False):
|
||||||
|
""" Save the replenish quantity and mark the cells as manually updated.
|
||||||
|
We override this to provide two-way (Trace up and Drill down) updating
|
||||||
|
of linked components in the BOM hierarchy.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Calculate the difference between new quantity and old quantity
|
||||||
|
date_start, date_stop = self.company_id._get_date_range(force_period=period_scale)[date_index]
|
||||||
|
existing_forecast = self.forecast_ids.filtered(lambda f:
|
||||||
|
f.date >= date_start and f.date <= date_stop)
|
||||||
|
|
||||||
|
old_qty = sum(existing_forecast.mapped('replenish_qty')) if existing_forecast else 0.0
|
||||||
|
|
||||||
|
# Call super first to update this component
|
||||||
|
res = super().set_replenish_qty(date_index, quantity, period_scale)
|
||||||
|
|
||||||
|
quantity = float_round(float(quantity), precision_rounding=self.product_uom_id.rounding)
|
||||||
|
diff_qty = quantity - old_qty
|
||||||
|
|
||||||
|
# If the quantity changed and we're not already propagating an update to avoid infinite loops
|
||||||
|
if diff_qty != 0 and not self.env.context.get('skip_two_way_mps'):
|
||||||
|
self._propagate_replenish_qty(diff_qty, date_index, period_scale)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def _propagate_replenish_qty(self, diff_qty, date_index, period_scale=False):
|
||||||
|
"""Propagate replenishment difference to parents (bottom-up) and children (top-down)"""
|
||||||
|
|
||||||
|
# We need to trace up (bottom-up)
|
||||||
|
parent_schedules = self._get_impacted_parent_schedules()
|
||||||
|
# We need to drill down (top-down)
|
||||||
|
child_schedules = self._get_impacted_child_schedules()
|
||||||
|
|
||||||
|
# Fetch the BOM hierarchy relationships
|
||||||
|
schedules_to_compute = parent_schedules | child_schedules | self
|
||||||
|
indirect_demand_trees = schedules_to_compute._get_indirect_demand_tree()
|
||||||
|
indirect_ratio_mps = schedules_to_compute._get_indirect_demand_ratio_mps(indirect_demand_trees)
|
||||||
|
|
||||||
|
# TRACE UP: Update parents
|
||||||
|
for parent_schedule in parent_schedules:
|
||||||
|
# Find the ratio: how many of 'self.product_id' are needed to make 1 'parent_schedule.product_id'
|
||||||
|
ratios = indirect_ratio_mps.get((parent_schedule.warehouse_id, parent_schedule.product_id), {})
|
||||||
|
ratio = ratios.get(self.product_id, 0.0)
|
||||||
|
|
||||||
|
if ratio > 0:
|
||||||
|
parent_diff = diff_qty / ratio
|
||||||
|
|
||||||
|
# Retrieve the existing parent quantity for this period
|
||||||
|
p_date_start, p_date_stop = parent_schedule.company_id._get_date_range(force_period=period_scale)[date_index]
|
||||||
|
p_exist = parent_schedule.forecast_ids.filtered(lambda f: f.date >= p_date_start and f.date <= p_date_stop)
|
||||||
|
p_old_qty = sum(p_exist.mapped('replenish_qty')) if p_exist else 0.0
|
||||||
|
|
||||||
|
new_parent_qty = p_old_qty + parent_diff
|
||||||
|
if new_parent_qty < 0:
|
||||||
|
new_parent_qty = 0.0
|
||||||
|
|
||||||
|
# Update parent with skip context to prevent loops
|
||||||
|
parent_schedule.with_context(skip_two_way_mps=True).set_replenish_qty(date_index, new_parent_qty, period_scale)
|
||||||
|
|
||||||
|
# DRILL DOWN: Update children
|
||||||
|
for child_schedule in child_schedules:
|
||||||
|
# Find the ratio: how many of 'child.product_id' base units are needed to make 1 'self.product_id' base unit
|
||||||
|
ratios = indirect_ratio_mps.get((self.warehouse_id, self.product_id), {})
|
||||||
|
ratio = ratios.get(child_schedule.product_id, 0.0)
|
||||||
|
|
||||||
|
if ratio > 0:
|
||||||
|
# child_diff is in base units
|
||||||
|
child_diff = diff_qty * ratio
|
||||||
|
|
||||||
|
# If the packaging module is used, we must convert the base unit diff_qty
|
||||||
|
# into the packaging unit diff_qty.
|
||||||
|
# Because child_schedule.set_replenish_qty will do: quantity = float(quantity) * self.packaging_id.qty
|
||||||
|
# Therefore, we must pass the quantity *in packaging units*.
|
||||||
|
if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id:
|
||||||
|
packaging_qty = child_schedule.packaging_id.qty
|
||||||
|
if packaging_qty:
|
||||||
|
child_diff = child_diff / packaging_qty
|
||||||
|
|
||||||
|
c_date_start, c_date_stop = child_schedule.company_id._get_date_range(force_period=period_scale)[date_index]
|
||||||
|
c_exist = child_schedule.forecast_ids.filtered(lambda f: f.date >= c_date_start and f.date <= c_date_stop)
|
||||||
|
|
||||||
|
# The existing replenish_qty in the database is actually stored in base units.
|
||||||
|
# BUT we need to pass the new value to set_replenish_qty in the *packaging* units if packaging is used.
|
||||||
|
c_old_qty_base = sum(c_exist.mapped('replenish_qty')) if c_exist else 0.0
|
||||||
|
|
||||||
|
if hasattr(child_schedule, 'packaging_id') and child_schedule.packaging_id and child_schedule.packaging_id.qty:
|
||||||
|
c_old_qty_pack = c_old_qty_base / child_schedule.packaging_id.qty
|
||||||
|
else:
|
||||||
|
c_old_qty_pack = c_old_qty_base
|
||||||
|
|
||||||
|
new_child_qty_pack = c_old_qty_pack + child_diff
|
||||||
|
if new_child_qty_pack < 0:
|
||||||
|
new_child_qty_pack = 0.0
|
||||||
|
|
||||||
|
child_schedule.with_context(skip_two_way_mps=True).set_replenish_qty(date_index, new_child_qty_pack, period_scale)
|
||||||
Loading…
Reference in New Issue
Block a user