Stock Reservation Issues in Odoo
Stock reservations hold inventory for specific orders. When they get stuck:
Issues:
- Product shows 100 On Hand but 0 Available (all reserved)
- Cancelled order still shows reserved quantity
- Cannot reserve stock for new orders despite physical availability
- "Unreserve" button does nothing
- Reserved quantity exceeds on-hand quantityUnderstanding Reservation in Odoo
# On Hand = total physical quantity in warehouse
# Reserved = quantity allocated to pending operations
# Available = On Hand - Reserved (what can be allocated to new orders)
# Reservations are tracked in stock.quant:
sudo -u postgres psql -d mydb -c "
SELECT sq.product_id, pt.name, sq.quantity, sq.reserved_quantity,
(sq.quantity - sq.reserved_quantity) as available,
sl.complete_name as location
FROM stock_quant sq
JOIN product_product pp ON sq.product_id = pp.id
JOIN product_template pt ON pp.product_tmpl_id = pt.id
JOIN stock_location sl ON sq.location_id = sl.id
WHERE sq.product_id = PRODUCT_ID
AND sl.usage = 'internal';
"Fix 1: Find What Is Reserving the Stock
# Find all active stock moves reserving this product:
sudo -u postgres psql -d mydb -c "
SELECT sm.id, sm.reference, sm.origin, sm.state,
sm.product_uom_qty, sm.quantity as reserved,
sp.name as picking_name, sp.state as picking_state
FROM stock_move sm
LEFT JOIN stock_picking sp ON sm.picking_id = sp.id
WHERE sm.product_id = PRODUCT_ID
AND sm.state IN ('assigned', 'partially_available', 'waiting', 'confirmed')
ORDER BY sm.date;
"
# This shows all pending operations holding reservations
# Look for cancelled or obsolete orders that still hold stockFix 2: Unreserve from Cancelled Orders
# If an order was cancelled but reservations remain:
# Via UI:
# 1. Inventory > Operations > Transfers
# 2. Filter by state = "Ready" or "Waiting"
# 3. Find the stuck transfer
# 4. Click "Unreserve" button
# 5. Then cancel the transfer
# If Unreserve button doesn't work:
# The move may be in an inconsistent state
# Reset via database:
sudo -u postgres psql -d mydb -c "
-- Find stuck moves
SELECT id, reference, state, product_uom_qty, quantity
FROM stock_move
WHERE product_id = PRODUCT_ID
AND state = 'assigned'
AND picking_id IN (
SELECT id FROM stock_picking WHERE state = 'cancel'
);
"
# Set stuck moves to cancelled:
sudo -u postgres psql -d mydb -c "
UPDATE stock_move SET state = 'cancel', quantity = 0
WHERE id IN (STUCK_MOVE_IDS);
-- Update the move lines too
UPDATE stock_move_line SET state = 'cancel', quantity = 0
WHERE move_id IN (STUCK_MOVE_IDS);
"Fix 3: Recalculate Quant Reservations
# If reserved_quantity on quants is wrong:
# The reservation count should equal the sum of all active move line reservations
sudo -u postgres psql -d mydb -c "
-- Calculate what reserved_quantity SHOULD be
SELECT sq.id, sq.quantity, sq.reserved_quantity,
COALESCE(SUM(sml.quantity), 0) as actual_reserved
FROM stock_quant sq
LEFT JOIN stock_move_line sml ON (
sml.product_id = sq.product_id
AND sml.location_id = sq.location_id
AND sml.lot_id IS NOT DISTINCT FROM sq.lot_id
AND sml.state IN ('assigned', 'partially_available')
)
WHERE sq.product_id = PRODUCT_ID
AND sq.location_id IN (SELECT id FROM stock_location WHERE usage = 'internal')
GROUP BY sq.id;
"
# Fix the quant reservation count:
sudo -u postgres psql -d mydb -c "
UPDATE stock_quant sq SET reserved_quantity = (
SELECT COALESCE(SUM(sml.quantity), 0)
FROM stock_move_line sml
WHERE sml.product_id = sq.product_id
AND sml.location_id = sq.location_id
AND sml.lot_id IS NOT DISTINCT FROM sq.lot_id
AND sml.state IN ('assigned', 'partially_available')
)
WHERE sq.product_id = PRODUCT_ID;
"Fix 4: Reservation Method Settings
# Odoo offers different reservation methods:
# Product > Inventory tab > Reservation Method:
# - "At Confirmation" — reserves when order is confirmed
# - "Manually" — reserves only when you click "Check Availability"
# - "Before scheduled date" — reserves X days before delivery
# If stock is reserved too early, change to Manual or Before scheduled date
# This frees stock for more urgent ordersFix 5: Forced Unreserve via Odoo Shell
# For stubborn reservations, use the ORM:
# odoo shell -d mydb
moves = env['stock.move'].search([
('product_id', '=', PRODUCT_ID),
('state', '=', 'assigned'),
])
for move in moves:
print(f"{move.reference}: {move.product_uom_qty} reserved")
# Unreserve specific moves:
# move._do_unreserve()
# Commit changes
env.cr.commit()Prevention
DeployMonkey's AI agent monitors reservation health and detects stuck reservations automatically. The agent validates reservation consistency and alerts when available stock drops unexpectedly due to orphaned reservations.