feat: Enhance consumed quantity locking for MO components, adding virtual record support and immediate locking on creation.

This commit is contained in:
Suherdy Yacob 2026-02-11 11:20:44 +07:00
parent 71db3eed46
commit 0285ce9ebe
2 changed files with 49 additions and 28 deletions

View File

@ -1,22 +1,21 @@
# MO Lock Consumed # MO Lock Consumed
This Odoo module customizes the behavior of Manufacturing Orders (MO) regarding ingredient consumption and validation. ## Overview
This module prevents the **Consumed Quantity** of Manufacturing Order components from being automatically reset or scaled by Odoo when:
1. The **Quantity to Produce** on the MO header is changed (System Scaling).
2. The MO is saved or "Produce" is clicked (System Refresh/Web Save).
## Features ## Features
- **Strict Locking**: Once a component's consumed quantity is set (manually or via "Produce"), it is **locked**.
- **Auto-Fill Support**: Allows the initial auto-fill of quantities (from BOM demand) when creating an MO, but locks them immediately after.
- **Virtual Record Support**: Protects quantities even on unsaved (New/Virtual) records, preventing unexpected reverts during creation.
- **Manual Override**: Users can still manually change the quantity (if positive), which will update the locked value. Only *system resets* (to 0) or *scaling* are blocked.
1. **Lock Consumed Quantity**: ## Technical Details
- Prevents the automatic update of a component's "Consumed" quantity when the MO's "Quantity to Produce" is changed, **IF**: - **`manual_consumption` Flag**: This flag is set to `True` whenever a move has a positive quantity set.
- The component already has a manually entered "Consumed" quantity (greater than 0). - **`_should_bypass_set_qty_producing`**: Overridden to return `True` if:
- The component is marked as **Picked** (manual consumption) or has **Manual Consumption** flag set. - `manual_consumption` is set.
- OR the record is "Virtual" (NewId) and has `quantity > 0`.
- If a component has 0 consumed quantity and is not picked, it will continue to scale automatically based on the BOM ratio (standard behavior). This bypasses Odoo's default scaling logic.
- **`write` Guard**: Blocks writing `quantity=0` to locked moves, preventing resets.
2. **Safety Check for Negative Stock**: - **`create` Check**: Locks new moves immediately if they are created with a specific `quantity` (Consumed), while ignoring `product_uom_qty` (Demand) to allow initial auto-fill.
- Hides the **"Produce"** and **"Produce All"** buttons if proceeding with the production would cause the potential stock of any component to drop below zero (negative stock).
- **Exception**: If a component's "Consumed" quantity is explicitly set to **0** (e.g., for custom productions where a BOM component is not used), it is skipped in this check, allowing production to proceed.
## Usage
- **Standard Flow**: Create an MO, confirm it. The behavior remains standard unless you manually intervene.
- **Custom Consumption**: If you manually set a component's consumed quantity (e.g., to 5 units), changing the global "Quantity to Produce" will **not** override your manual entry of 5 units.
- **Stock Validation**: If you try to produce a quantity that requires more components than you have in stock, the Produce buttons will disappear, preventing you from accidentally forcing negative stock. To fix this, either replenish stock or adjust the consumed quantity to 0 (if omitting the component).

View File

@ -1,4 +1,4 @@
from odoo import models from odoo import models, api
class StockMove(models.Model): class StockMove(models.Model):
_inherit = 'stock.move' _inherit = 'stock.move'
@ -8,24 +8,46 @@ class StockMove(models.Model):
Only bypass if explicitly flagged as manual consumption. Only bypass if explicitly flagged as manual consumption.
We rely on the write() method to set this flag and to block resets. We rely on the write() method to set this flag and to block resets.
""" """
if self.sudo().manual_consumption: # Remove sudo() to allow NewId (virtual records) to access their cache.
# If we use sudo(), it might force a DB read which fails for NewId or returns old data.
allow_bypass = self.manual_consumption
# NewId Fix: If it's a virtual record (NewId) and has quantity, it's "locked" logic-wise
# because the user (or auto-fill) has set a value, and we don't want scaling to wipe it.
# Real IDs are integers. Virtual IDs (NewId) are not.
if not allow_bypass and self.quantity > 0 and not isinstance(self.id, int):
allow_bypass = True
if allow_bypass:
return True return True
return super()._should_bypass_set_qty_producing() return super()._should_bypass_set_qty_producing()
def write(self, vals): def write(self, vals):
# 1. Universal Zero-Guard: Block reset to 0 for MO components that have content # 1. Zero-Guard: Block resets to 0 if Locked
# We ALLOW positive updates so users can manually set consumption.
# We rely on _should_bypass_set_qty_producing (via manual_consumption=True)
# to prevent the System from generating Scaling updates.
if 'quantity' in vals and vals['quantity'] == 0: if 'quantity' in vals and vals['quantity'] == 0:
# We filter for moves that are components and have 'something' (qty or lines) locked_moves = self.filtered(lambda m: m.manual_consumption)
# and are NOT being cancelled/scrapped (check context or state if needed, but simple is better for now) if locked_moves and len(locked_moves) == len(self):
protected = self.filtered(lambda m: m.raw_material_production_id and (m.quantity > 0 or m.move_line_ids))
if protected:
# Remove quantity from vals to prevent reset
vals = dict(vals)
del vals['quantity'] del vals['quantity']
# 2. Enforce flags for positive updates # 2. Arm the Lock
if 'quantity' in vals and vals['quantity'] > 0: if 'quantity' in vals and vals.get('quantity', 0) > 0:
vals['manual_consumption'] = True vals['manual_consumption'] = True
vals['picked'] = True vals['picked'] = True
return super().write(vals) return super().write(vals)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
# Look at Consumed (quantity) only.
# If we lock on Demand (product_uom_qty), we block the initial auto-fill.
consumed = vals.get('quantity', 0)
if consumed > 0:
# Lock Immediately if created with a specific consumed quantity
vals['manual_consumption'] = True
vals['picked'] = True
return super().create(vals_list)