Movimientos internos simplificados de stock en Odoo
Extendiendo el módulo stock_move_location
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">
<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.