[ADD] stock_move_manual_quantity: a hand-typed quantity is the picked one
An operator weighing a basket types 0.87 kg where the member ordered 1 kg, and a few hours later the line is back at 1 kg. Raising the quantity never reverts. The asymmetry is the reservation engine: below the demand the move goes back to `partially_available` (`_recompute_state`), which is exactly the state `_action_assign` looks for, while above it stays `assigned` and matches no domain. Two things call `_action_assign` on their own: the `Procurement: run scheduler` cron, and `_trigger_assign`, which runs every time another transfer of the same product is validated -- the second explains the "a few hours" better than a daily cron. Both respect one flag only, `picked`, and nobody was setting it: the checkbox is `optional="hide"` on the transfer form and absent from the detailed operation lists. So an onchange on `quantity` marks the record as picked. Onchanges only run from the interface, never from the reservation engine, so a manual edit is told apart from a reservation without guessing at contexts, and if the onchange ever stops firing the behaviour degrades to today's instead of freezing reservations across the system. `picked` also keeps `_free_reservation` from stealing the quantity when stock runs short elsewhere. The three lists where a quantity can be typed now load the field, since the client only sends back what the arch declares. When the typed quantity is below the demand, the demand follows it down. The move stays `assigned` and no backorder is asked for a weight that will never be completed. Only downwards: above the demand nothing needs adjusting. A line left at zero keeps its demand, so a transfer still offers its usual backorder choice for what was not delivered, and chained moves are only frozen -- lowering their demand would leave the next step of the route asking for more than this one delivers. `_pre_action_done_hook` needs the counterpart. Core auto-picks a transfer only when *no* move is picked yet, so freezing one weighed line would push the untouched ones to a backorder, or cancel them if the user answers "no backorder", with the goods already in the basket. Picking everything that carries a quantity keeps validation exactly as operators know it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cb32fb6c0d
commit
d433e50f2f
15 changed files with 436 additions and 0 deletions
60
stock_move_manual_quantity/models/stock_move.py
Normal file
60
stock_move_manual_quantity/models/stock_move.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Copyright 2026 Criptomart
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api
|
||||
from odoo import models
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = "stock.move"
|
||||
|
||||
@api.onchange("quantity")
|
||||
def _onchange_quantity_manual(self):
|
||||
"""Mark the move as picked when a user types its quantity.
|
||||
|
||||
Covers the ``Operations`` tab of a transfer, where the quantity is
|
||||
edited on the move and ``_set_quantity`` spreads it to the lines.
|
||||
"""
|
||||
for move in self:
|
||||
if move.state not in ("done", "cancel"):
|
||||
move.picked = True
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if "quantity" in vals and vals.get("picked"):
|
||||
self._align_demand_to_manual_quantity()
|
||||
return res
|
||||
|
||||
def _align_demand_to_manual_quantity(self):
|
||||
"""Lower the demand of a move to the quantity a user typed by hand.
|
||||
|
||||
A move whose quantity is below its demand goes back to
|
||||
``partially_available`` (``_recompute_state``), and that is precisely
|
||||
what makes the scheduler reserve the difference again. Aligning the
|
||||
demand keeps the move ``assigned`` and drops the backorder question for
|
||||
a quantity that is final: a weighed product is never completed later.
|
||||
|
||||
The demand is only ever lowered. Above it the move is already
|
||||
``assigned``, so nothing has to be touched.
|
||||
"""
|
||||
for move in self:
|
||||
if move.state in ("draft", "done", "cancel") or move.is_inventory:
|
||||
continue
|
||||
if move.move_dest_ids:
|
||||
# Chained moves: lowering the demand here would leave the next
|
||||
# step of the route asking for more than this one delivers.
|
||||
continue
|
||||
rounding = move.product_uom.rounding
|
||||
if float_is_zero(move.quantity, precision_rounding=rounding):
|
||||
# Nothing was picked: leave the demand alone so the transfer
|
||||
# still offers its usual backorder choice.
|
||||
continue
|
||||
if (
|
||||
float_compare(
|
||||
move.quantity, move.product_uom_qty, precision_rounding=rounding
|
||||
)
|
||||
< 0
|
||||
):
|
||||
move.product_uom_qty = move.quantity
|
||||
Loading…
Add table
Add a link
Reference in a new issue