feat: Enhance consumed quantity locking for MO components, adding virtual record support and immediate locking on creation.
This commit is contained in:
parent
71db3eed46
commit
0285ce9ebe
33
README.md
33
README.md
@ -1,22 +1,21 @@
|
||||
# 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
|
||||
- **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**:
|
||||
- Prevents the automatic update of a component's "Consumed" quantity when the MO's "Quantity to Produce" is changed, **IF**:
|
||||
- The component already has a manually entered "Consumed" quantity (greater than 0).
|
||||
- The component is marked as **Picked** (manual consumption) or has **Manual Consumption** flag set.
|
||||
|
||||
- If a component has 0 consumed quantity and is not picked, it will continue to scale automatically based on the BOM ratio (standard behavior).
|
||||
|
||||
2. **Safety Check for Negative Stock**:
|
||||
- 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).
|
||||
## Technical Details
|
||||
- **`manual_consumption` Flag**: This flag is set to `True` whenever a move has a positive quantity set.
|
||||
- **`_should_bypass_set_qty_producing`**: Overridden to return `True` if:
|
||||
- `manual_consumption` is set.
|
||||
- OR the record is "Virtual" (NewId) and has `quantity > 0`.
|
||||
This bypasses Odoo's default scaling logic.
|
||||
- **`write` Guard**: Blocks writing `quantity=0` to locked moves, preventing resets.
|
||||
- **`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.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from odoo import models
|
||||
from odoo import models, api
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
@ -8,24 +8,46 @@ class StockMove(models.Model):
|
||||
Only bypass if explicitly flagged as manual consumption.
|
||||
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 super()._should_bypass_set_qty_producing()
|
||||
|
||||
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:
|
||||
# We filter for moves that are components and have 'something' (qty or lines)
|
||||
# and are NOT being cancelled/scrapped (check context or state if needed, but simple is better for now)
|
||||
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)
|
||||
locked_moves = self.filtered(lambda m: m.manual_consumption)
|
||||
if locked_moves and len(locked_moves) == len(self):
|
||||
del vals['quantity']
|
||||
|
||||
# 2. Enforce flags for positive updates
|
||||
if 'quantity' in vals and vals['quantity'] > 0:
|
||||
# 2. Arm the Lock
|
||||
if 'quantity' in vals and vals.get('quantity', 0) > 0:
|
||||
vals['manual_consumption'] = True
|
||||
vals['picked'] = True
|
||||
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user