# 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