← Voltar ao blog

Salable quantity errada no Magento 2? Como funcionam as reservations

Rodrigo Biazzi

"O produto tem 10 em estoque, mas a loja diz que está esgotado." Com MSI, essa frase normalmente significa que a salable quantity é menor que a quantity, e a diferença são as reservations. Este artigo explica como as reservations funcionam no Magento Open Source e no Adobe Commerce 2.4.8, por que elas se desalinham e como corrigir sem editar o banco na mão.

Se sources e stocks são novidade para você, leia antes MSI sources e stocks. As reservations ficam ligadas aos stocks, não aos sources.

Quantity versus salable quantity

No grid de produtos do admin você vê duas colunas. Quantity per Source é o que fisicamente está em cada source, segundo os source items. Salable Quantity é o que cada stock ainda pode vender. Elas são calculadas de formas diferentes:

  • Parte-se da soma das quantidades dos source items em estoque nos sources ligados ao stock.
  • Subtrai-se o out of stock threshold, configurado globalmente ou por produto.
  • Soma-se o total de todas as reservations daquele SKU e stock. As reservations de pedidos em aberto são negativas, então isso reduz o número.

Quando a salable quantity chega a zero, o produto não é vendável nos websites que usam aquele stock, mesmo que o source ainda mostre quantidade em mãos.

O que é uma reservation

Uma reservation é uma linha em inventory_reservation com um stock ID, um SKU, uma quantidade e um campo JSON de metadata que diz qual evento a criou e para qual objeto, normalmente um pedido. As reservations são append only. O Magento nunca atualiza uma reservation. Ele adiciona uma nova com o sinal oposto.

O ciclo de vida de um pedido normal é este:

  1. Pedido feito. Uma reservation de menos a quantidade pedida é adicionada. A salable quantity cai. A quantidade do source não muda.
  2. Pedido enviado. A quantidade enviada é deduzida dos source items escolhidos, e uma reservation de mais a quantidade enviada é adicionada. A soma das reservations daquele pedido agora é zero, e a queda passou das reservations para o source.
  3. Pedido cancelado. Uma reservation de mais a quantidade cancelada é adicionada. A salable quantity volta.
  4. Credit memo com retorno ao estoque. A quantidade é devolvida ao source.

Quando as reservations de um pedido somam zero, elas não servem mais para nada. Um cron job remove as linhas de reservation cuja soma por SKU e stock é zero, para a tabela não crescer para sempre.

Por que as reservations se desalinham

As reservations só estão corretas se todo pedido passa pelo ciclo completo dentro do Magento. Na prática, quebra de alguns jeitos previsíveis:

  • Pedidos concluídos sem shipment. Um pedido marcado como complete ou closed por código ou por um módulo, sem criar um shipment, nunca recebe a reservation compensatória. O estoque fica reservado para sempre.
  • Pedidos apagados do banco. Apagar pedidos de teste com SQL deixa as reservations deles para trás.
  • Estoque definido por um ERP que já desconta pedidos em aberto. Se o ERP envia a quantidade já líquida dos pedidos em aberto, e o Magento também subtrai as reservations, os pedidos em aberto são contados duas vezes.
  • Shipments criados num sistema externo. Se o ERP envia e só atualiza o status do pedido no Magento, a dedução e a compensação não acontecem no Magento.
  • Consumers parados. Algumas atualizações de inventário passam por consumers de message queue. Se eles não rodam, atualizações de reservation vindas de ações em massa podem atrasar ou se perder.

Encontrando inconsistências

O Magento traz um comando que compara reservations com pedidos e lista as divergências:

bin/magento inventory:reservation:list-inconsistencies

Ele reporta pedidos cujas reservations não fecham com o que o estado do pedido diz. Opções úteis:

  • -c ou --complete-orders lista só pedidos em estado final, complete, closed ou canceled, que não deveriam ter reservation aberta.
  • -i ou --incomplete-orders lista só pedidos ainda em andamento.
  • -r ou --raw imprime o resultado num formato compacto pensado para ser encadeado no próximo comando.
  • -b define o tamanho do bunch para tabelas de pedidos grandes.

Para ver os totais direto no banco, esta query somente leitura lista SKUs com reservations abertas por stock:

SELECT stock_id, sku, SUM(quantity) AS reserved
FROM inventory_reservation
GROUP BY stock_id, sku
HAVING SUM(quantity) <> 0
ORDER BY reserved ASC
LIMIT 50;

Um número negativo grande num SKU sem pedidos em aberto é um sinal claro de desalinhamento.

Corrigindo com compensations

A correção é adicionar as reservations compensatórias que deveriam ter sido criadas. O Magento tem um comando para isso, e ele lê a saída raw do primeiro:

bin/magento inventory:reservation:list-inconsistencies -r \
  | bin/magento inventory:reservation:create-compensations

Eu costumo rodar primeiro para pedidos complete, porque esses não deixam dúvida. Um pedido complete ou cancelado deveria ter zero reservado:

bin/magento inventory:reservation:list-inconsistencies -c -r \
  | bin/magento inventory:reservation:create-compensations

Para pedidos incompletos, olhe antes de compensar. Um pedido em processing que será enviado normalmente deve manter a reservation. Compensá-lo faz o estoque parecer maior do que é, e você corre o risco de vender além do que tem. A lista de incompletos é mais útil para achar pedidos travados num estado de onde ninguém vai tirá-los.

Faça um backup do banco antes e rode no staging com uma cópia recente, como em qualquer correção de dados. O comando só adiciona linhas em inventory_reservation, não altera pedidos nem sources. Depois de rodar, o cron de limpeza remove as linhas que agora somam zero.

Por que não dar um truncate na tabela?

Truncar a inventory_reservation remove também as reservations de todos os pedidos em aberto. Todos os produtos pedidos e ainda não enviados voltam a ser vendáveis com a quantidade cheia do source, o que leva a vender além do estoque até esses pedidos serem enviados. Só é aceitável numa loja sem pedidos em aberto, o que raramente é o caso de uma loja no ar.

Prevenindo o desalinhamento

  • Crie os shipments no Magento, mesmo quando o fulfillment acontece em outro lugar. O ERP pode chamar o endpoint de ship order, POST /V1/order/{orderId}/ship, com o código do source, em vez de só definir o status. Assim a dedução e a compensação acontecem num lugar só.
  • Combine quem é dono da quantidade disponível. Se o ERP envia o estoque físico, o Magento subtrai os pedidos em aberto pelas reservations. Se o ERP envia o estoque disponível, os dois vão contar em dobro. Escolha um modelo e documente. O lado da integração está em padrões de integração com ERP.
  • Nunca apague pedidos com SQL. Se pedidos de teste precisam sair, cancele antes, para as reservations deles serem compensadas.
  • Mantenha cron e consumers rodando e crie alerta para quando pararem. Veja o guia de cron jobs.
  • Agende a checagem. Rodar o comando de listagem para pedidos complete uma vez por semana e alertar sobre qualquer saída pega problemas cedo.

Lendo a salable quantity no código

Quando um código customizado precisa saber se um produto pode ser vendido, use a API de inventory sales em vez de ler quantidades das tabelas. Magento\InventorySalesApi\Api\GetProductSalableQtyInterface retorna a salable quantity de um SKU e stock ID, já incluindo as reservations e o threshold. Resolva o stock ID do website com StockResolverInterface. Ler o qty do stock item legado dá a quantidade do source padrão, que é o número errado para essa pergunta.

FAQ

Por que a salable quantity é menor que a quantity no Magento 2?

A salable quantity subtrai as reservations de pedidos feitos e ainda não enviados nem cancelados, mais o out of stock threshold. A diferença é esperada para pedidos em aberto.

É seguro apagar linhas da inventory_reservation?

Em geral não. Apagar linhas de pedidos em aberto torna o estoque deles vendável de novo e pode causar venda além do estoque. Use o comando de create compensations, que adiciona linhas de balanceamento em vez disso.

Por que as reservations não são limpas depois que o pedido é concluído?

A reservation compensatória é adicionada quando um shipment é criado ou o pedido é cancelado. Pedidos fechados sem shipment mantêm a reservation negativa até você compensar.

O cron de limpeza corrige reservations erradas?

Não. Ele só remove linhas que já somam zero por SKU e stock. Reservations erradas têm soma diferente de zero e ficam até serem compensadas.

Atendo como freelancer em Magento e Adobe Commerce, incluindo problemas de inventário e fluxo de pedidos. Se precisa de ajuda com isso, fale comigo.

Artigos relacionados

Erros comuns do Magento 2 e como corrigir

Correções para erros clássicos do Magento 2: Area code is not set, Class does not exist, Invalid Form Key, indexers travados, unserialize e 404 em estáticos.