Movimientos internos simplificados de stock en Odoo

Extendiendo el módulo stock_move_location

Gustavo Orrillo
- 20/06/2022 - 3 min. de lectura

En el repositorio de OCA hay un módulo que simplifica el movimiento interno de stock; estoy hablando del módulo stock_move_location. Dicho módulo es muy bueno porque brinda una interface muy simplificada para realizar movimientos internos de stock. Pese a ser simple, es muy eficaz. Permitiendo solo el movimiento de quants existentes en el sistema, evita el stock negativo y ademas reduce de forma importante el error de los usuarios.

Es por ello que extendimos dicho módulo y creamos otro módulo llamado stock_move_location_product. Este módulo extiende el módulo move_stock_location y brinda la posibilidad de filtrar los movimientos por ubicación origen, producto y número de lote/serie.


Este wizard permite crear la transferencia para el producto y lote seleccionados. Permite hacer la transferencia de forma inmediata o crearla para ejecutarla a posterior (botón Transferencia Planificada)

Notas técnicas

Este módulo solo extiendo el wizard creado en el módulo stock_move_location. Primero agrega tres campos necesarios: product_id (que se relaciona con el modelo product.product), lot_id (relacionado con stock.production.lot) y tracking. Nada especial con estos grupos. Lo que es interesante es la extensión de la vista, donde se agrega un grupo antes de las líneas de los quants a mover. Esto lo hacemos mediante el xpath

<xpath expr="//group[@name='lines']" position="before">
Luego, tanto el campo product_id y lot_id tienen seteado el atributo no_create y no_create_edit para no permitir la creación de products/números de serie al momento de edición:

<field name="product_id" 
        options="{'no_create': True, 'no_create_edit': True}" />

 




Después, si observan la definición del campo lot_id se verá que el mismo tiene como domain al producto seleccionado en el formulario:

<field name="lot_id" 
            attrs="{'invisible': [('tracking','=','none')]}"
            domain="[('product_id','=',product_id)]"
            options="{'no_create': True, 'no_create_edit':True}"
            />


Y se puede ver que se setea el atributo de invisible a verdadero si el valor del campo tracking (el cual cambia por medio de un onchange del product_id) es igual a none

@api.onchange("origin_location_id","product_id","lot_id")
def onchange_origin_location(self):
    # Get origin_location_disable context key to prevent load all origin
    # location products when user opens the wizard from stock quants to
    # move it to other location.
    if self.product_id:
        self.tracking = self.product_id.tracking

También se redefine el método onchange_origin_location modificando el decorador para que tome en cuenta el producto y el lote

@api.onchange("origin_location_id","product_id","lot_id")

Y se redefine el método _get_group_quants que se invoca cada vez que se cambia la ubicación de origen, el producto y el lote. En este método, tenemos según si la ubicación, producto y lote fueron seleccionados; diferentes queries de SQL para obtener los quants disponibles. Nótese que la consulta no se hace por medio del ORM, sino por medio de SQL (lo cual es más rápido):

def _get_group_quants(self):
    location_id = self.origin_location_id
    # Using sql as search_group doesn't support aggregation functions
    # leading to overhead in queries to DB
    if not self.product_id and not self.lot_id:
        query = """
            SELECT product_id, lot_id, SUM(quantity) AS quantity,
                SUM(reserved_quantity) AS reserved_quantity
                FROM stock_quant
                WHERE location_id = %s
                GROUP BY product_id, lot_id
            """
        self.env.cr.execute(query, (location_id.id,))
    elif self.product_id and not self.lot_id:
        query = """
            SELECT product_id, lot_id, SUM(quantity) AS quantity,
                SUM(reserved_quantity) AS reserved_quantity
                FROM stock_quant
                WHERE location_id = %s
                AND product_id = %s
                GROUP BY product_id, lot_id
            """
        self.env.cr.execute(query, (location_id.id, self.product_id.id,))
    elif self.product_id and self.lot_id:
        query = """
            SELECT product_id, lot_id, SUM(quantity) AS quantity,
                SUM(reserved_quantity) AS reserved_quantity
                FROM stock_quant
                WHERE location_id = %s
                AND product_id = %s
                AND lot_id = %s
                GROUP BY product_id, lot_id
            """
        self.env.cr.execute(query, (location_id.id, self.product_id.id, self.lot_id.id, ))
    return self.env.cr.dictfetchall()


Por último, veamos la función (en el módulo stock_move_location) que crea la transferencia y la prepara o valida:

def action_move_location(self):
    self.ensure_one()
    if not self.picking_id:
        picking = self._create_picking()
    else:
        picking = self.picking_id
    self._create_moves(picking)
    if not self.env.context.get("planned"):
        moves_to_reassign = self._unreserve_moves()
        picking.button_validate()
        moves_to_reassign._action_assign()
    else:
        picking.action_confirm()
        picking.action_assign()
    self.picking_id = picking
    return self._get_picking_action(picking.id)

En otro post vamos a hablar estos movimientos, ya que toca temas muy importantes. Por ejemplo el movimiento de items reservados o no. Y como confirmar, asignar y validar los movimientos de stock. Eso es suficiente para otro post, y fundamental para comprender como funciona el módulo de stock.

Acerca de:

Gustavo Orrillo

Passionate about programming, he has implemented Odoo for different types of businesses since 2010. In Moldeo Interactive he is a founding Partner and Programmer; In addition to writing on the Blog about different topics related to the developments he makes.