[16.0][ADD] stock_available_immediately_mrp: fix kit immediately_usable_qty#74
Open
alvaro-gmz wants to merge 1 commit into
Open
[16.0][ADD] stock_available_immediately_mrp: fix kit immediately_usable_qty#74alvaro-gmz wants to merge 1 commit into
alvaro-gmz wants to merge 1 commit into
Conversation
bd4fd48 to
77bf2ca
Compare
…able_qty When ``stock_available_immediately`` and ``mrp`` are both installed, the formula ``immediately_usable_qty = virtual_available - incoming_qty`` applied to the aggregated kit fields produces inconsistent results, because each aggregated kit field (``virtual_available``, ``incoming_qty``, ...) is computed by Odoo MRP as an independent ``min`` over the components and may be limited by different components. Subtracting two such mins is not equivalent to ``qty_available - outgoing_qty`` for kits, even though that algebraic cancellation does hold for plain products. Example ------- Kit with two components, ``need=2`` each: Component A: qty=8, in=133, out=14 → virtual=127, immediately=-6 Component B: qty=4258, in=60, out=168 → virtual=4150, immediately=4090 Forecasted (kit) = min(127/2, 4150/2) = 63 ← limited by A Incoming (kit) = min(133/2, 60/2) = 30 ← limited by B immediately_usable_qty (current bug) = 63 - 30 = 33 ← inflated immediately_usable_qty (expected) = min(-6/2, 4090/2) = -3 Fix --- Recompute ``immediately_usable_qty`` for products with a phantom BoM directly from the components and aggregate the result with ``min(component / qty_per_kit) * bom.product_qty``, rounded DOWN to the kit UoM rounding (consistently with ``stock_available_mrp.potential_qty``). The other kit fields (``qty_available``, ``virtual_available``, ``incoming_qty``, ``outgoing_qty``, ``free_qty``) are intentionally left untouched and keep the standard Odoo MRP aggregation as ``min(component / need)`` per field. Why a glue module ----------------- The bug only manifests at the intersection of ``stock_available_immediately`` and ``mrp``: without ``stock_available_immediately`` the broken subtraction is not applied at all, and without ``mrp`` there are no phantom BoMs to aggregate. Putting the fix in either parent module would either bundle unrelated semantics or rely on a soft dependency that prevents using the ``bom_ids.*`` paths in ``@api.depends``. A glue with ``auto_install=True`` expresses the contract honestly: it appears only when both parents are present and stays out of the way otherwise. Interaction with stock_available_mrp ------------------------------------ This module is independent of ``stock_available_mrp`` and does not depend on it. They compose cleanly when both are installed: * This module fixes the ``immediately_usable_qty`` base value for kits. * ``stock_available_mrp`` then increments it with ``potential_qty`` (the quantity that can be manufactured from currently-available components). The end result for kits with both modules installed is therefore ``correct_kit_immediately + potential_qty`` instead of the previous ``buggy_kit_immediately + potential_qty``. Implementation notes -------------------- The component value is read from the ``res`` dict already produced by ``super()`` in the same call, instead of recursing through the attribute. This keeps the kit and its components consistent under contexts that other modules may install during ``_compute_available_quantities_dict`` (e.g. location/batch context wrappers), where a recursive attribute access could resolve under a different context than the batch. When the BoM line UoM and the component UoM live in different UoM categories, ``_compute_quantity(raise_if_failure=False)`` returns the original value unchanged, which would produce an inconsistent ratio. An explicit category check skips the component with a warning. A separate guard skips components whose BoM line declares zero quantity. The ``@api.depends`` of the compute tracks ``bom_ids.bom_line_ids``, ``bom_ids.bom_line_ids.product_id`` and ``bom_ids.bom_line_ids.product_qty`` (plus the variant-specific paths) so the kit cache is invalidated when a component is replaced or its quantity changes. Tests ----- 12 tests covering: - Unbalanced components reproducing the bug (negative result). - Balanced components — no change vs. previous behaviour. - All-positive components — standard healthy case. - Oversold components — propagation to the kit value. - Plain products without BoM — not affected (regression). - Manufacturing BoM (non-phantom) — not affected (regression). - Consumable components — skipped in the ratio. - Batch read of kit and components together — value consistent with ``super()`` output, regardless of context overlays. - BoM line with zero quantity — component skipped with warning. - BoM line with UoM in a different category than the component — skipped. - ``bom.product_qty != 1`` — scaling factor correctly applied. - UoM rounding contract — DOWN to kit UoM rounding.
77bf2ca to
5c0e2f3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new glue module
stock_available_immediately_mrpthat fixesimmediately_usable_qtyon products with a phantom BoM (kits) when bothstock_available_immediatelyandmrpare installed. The module auto-installs whenever both parents are present.The bug
For products with a phantom BoM,
stock_available_immediatelyapplies the formulaimmediately_usable_qty = virtual_available - incoming_qtyto the aggregated kit fields. Each aggregated kit field (virtual_available,incoming_qty, ...) is computed by Odoo MRP as an independentminover the components and may be limited by different components. Subtracting two such mins is not equivalent toqty_available - outgoing_qtyfor kits, even though that algebraic cancellation does hold for plain products.Concrete example
Kit with two components,
need=2each:The component-level cancellation (
qty - outper component) is correct, but the kit-level subtraction of two independent mins is not.The fix
Recompute
immediately_usable_qtyfor products with a phantom BoM directly from the components and aggregate the result withmin(component / qty_per_kit) * bom.product_qty, rounded DOWN to the kit UoM rounding (consistently withstock_available_mrp.potential_qty).The other kit fields (
qty_available,virtual_available,incoming_qty,outgoing_qty,free_qty) are intentionally left untouched and keep the standard Odoo MRP aggregation asmin(component / need)per field.Why a glue module
The bug only manifests at the intersection of
stock_available_immediatelyandmrp. Putting the fix in either parent would either bundle unrelated semantics or rely on a soft dependency that prevents using thebom_ids.*paths in@api.depends. A glue withauto_install=Trueexpresses the contract honestly: it appears only when both parents are present and stays out of the way otherwise.Interaction with stock_available_mrp
The module is independent of
stock_available_mrpand does not depend on it. They compose cleanly when both are installed:immediately_usable_qtybase value for kits.stock_available_mrpthen increments it withpotential_qty(the quantity that can be manufactured from currently-available components).The end result for kits with both modules installed is therefore
correct_kit_immediately + potential_qtyinstead of the previousbuggy_kit_immediately + potential_qty.Implementation notes
resdict already produced bysuper()in the same call, instead of recursing through the attribute. This keeps the kit and its components consistent under contexts that other modules may install during_compute_available_quantities_dict(e.g. location/batch context wrappers), where a recursive attribute access could resolve under a different context than the batch._compute_quantity(raise_if_failure=False)returns the original value unchanged, which would produce an inconsistent ratio. An explicit category check skips the component with a warning. A separate guard skips components whose BoM line declares zero quantity.@api.dependsof the compute tracksbom_ids.bom_line_ids,bom_ids.bom_line_ids.product_idandbom_ids.bom_line_ids.product_qty(plus the variant-specific paths) so the kit cache is invalidated when a component is replaced or its quantity changes.Tests
12 tests covering:
super()output, regardless of context overlays.bom.product_qty != 1— scaling factor correctly applied.Related
This bug surfaced in production when exporting kit stock to a marketplace aggregator: kits with components in different states reported phantom positive availability instead of the real (often negative) figure, leading to oversells.