# Copyright 2026 Criptomart # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import api from odoo import models class StockMoveLine(models.Model): _inherit = "stock.move.line" @api.onchange("quantity") def _onchange_quantity_manual(self): """A quantity typed by a user is the quantity physically handled. Onchanges only run from the user interface, never from the reservation engine, so this marks the lines a person edited without freezing the ones ``_action_assign`` reserves on its own. """ for line in self: if line.state not in ("done", "cancel"): line.picked = True def write(self, vals): res = super().write(vals) # `_onchange_quantity_manual` makes the interface send both fields in # the same write, which is what tells a manual edit from a reservation. if "quantity" in vals and vals.get("picked"): self._freeze_manual_quantity() return res def _freeze_manual_quantity(self, align_demand=True): """Protect a hand-typed quantity from the reservation engine. ``_action_assign`` (run by the ``Procurement: run scheduler`` cron and by ``_trigger_assign`` whenever another transfer of the same product is validated) tops up every move that is not picked until it reaches its demand, and ``_free_reservation`` steals quantities from move lines that are not picked. Marking the whole move keeps both away from it. """ lines = self.filtered(lambda ml: ml.state not in ("done", "cancel")) if not lines: return siblings = (lines.move_id.move_line_ids - lines).filtered( lambda ml: not ml.picked and ml.state not in ("done", "cancel") ) if siblings: # `_action_done` unlinks the non picked lines of a picked move. siblings.picked = True if align_demand: lines.move_id._align_demand_to_manual_quantity()