Salable quantity errada no Magento 2? Como funcionam as reservations
"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:
- Pedido feito. Uma reservation de menos a quantidade pedida é adicionada. A salable quantity cai. A quantidade do source não muda.
- 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.
- Pedido cancelado. Uma reservation de mais a quantidade cancelada é adicionada. A salable quantity volta.
- 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:
-cou--complete-orderslista só pedidos em estado final, complete, closed ou canceled, que não deveriam ter reservation aberta.-iou--incomplete-orderslista só pedidos ainda em andamento.-rou--rawimprime o resultado num formato compacto pensado para ser encadeado no próximo comando.-bdefine 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
MSI no Magento 2: sources, stocks e sales channels
Como funciona o Multi-Source Inventory do Magento 2: sources, stocks, sales channels, source selection, as tabelas por trás e como atualizar quantidades por código.
Cron no Magento 2: configuração, jobs customizados e troubleshooting
Como o cron do Magento 2 realmente funciona: crontab, cron groups, jobs customizados, a tabela cron_schedule, queue consumers e como corrigir jobs que nunca rodam.
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.
Magento 2 Catalog Price Rules não aplicam? Como funciona a indexação
Como as catalog price rules do Magento 2 são indexadas, por que uma regra não aparece na storefront, o cron diário, timezones e como depurar preços de regra com SQL.