Reserve Stock in Odoo Before Sales Order Confirmation

Optimising Odoo Stock Reservation

By default, Odoo reserves stock only after a Sales Order is confirmed. This ensures inventory is allocated only for confirmed deals. However, some businesses need to reserve stock earlier—for example, when handling pre-orders, quotations for priority customers, or industries where demand changes rapidly.

In this article, we’ll show you how to extend Odoo Sales to reserve stock before confirmation using a custom method. We’ll also explain how to release reserved stock if the quotation is cancelled.

Why Reserve Stock Before Confirmation?

Priority customers

Guarantee availability before the deal is finalised.

High-demand items

Prevent overselling or stock-outs.

Internal approvals

Reserve stock while waiting for management/customer approval.

Better planning

Sales and operations teams know that inventory is already blocked.

Default Odoo Limitation

By default, Odoo only reserves stock after a Sales Order is confirmed. Quotations do not block inventory, which can cause issues when multiple users create quotations simultaneously.

Custom Implementation: Reserve Before Confirmation

We extend Odoo with:

  • A Reserve Stock button on quotations.
  • A custom action_reserve_stock() method to trigger stock rules early.
  • An action_confirm() method to Unreserve Stock on Sales Confirmation.
  • A custom action_cancel() method to release stock when a quotation is cancelled.

1. Reserve Stock on Quotation

To reserve stock on a quotation, we define a custom action_reserve_stock() method that checks availability and creates stock moves:

class SaleOrder(models.Model):
    _inherit = "sale.order"
    reserved = fields.Boolean(string="Stock Reserved", default=False)
    state = fields.Selection(selection_add=[('reservation', "Reserved")])
def action_reserve_stock(self):
    """
    Reserve stock for all products in the sales order *before confirmation*.
    This method:
        - Validates available stock for each product.
        - Finds free (unreserved) quants.
        - Marks those quants as reserved by setting `reserved_quantity = 1.0`.
        - Updates the order state to "reservation".
    """
    Quant = self.env['stock.quant']
    for order in self:
        if order.reserved:
            raise UserError("Stock is already reserved for this quotation.")
        # --- Pre-check: Ensure all required products have enough free stock ---
        insufficient_lines = order.order_line.filtered(
            lambda line: line.product_id.detailed_type == 'product'
            and line.product_uom_qty > line.product_id.free_qty
        )
        if insufficient_lines:
            # Build a detailed error message
            error_msg = "\n".join(
                f"{line.product_id.display_name}: Requested {line.product_uom_qty}, Available {line.product_id.free_qty}"
                for line in insufficient_lines
            )
            raise UserError(f"Insufficient stock for these products:\n\n{error_msg}")
        # --- Reserve stock for each order line ---
        for line in order.order_line:
            if line.product_id.detailed_type != 'product' or line.product_uom_qty <= 0:
                continue  # Skip services or invalid quantities
            # Find free (unreserved) quants
            domain = [
                ('product_id', '=', line.product_id.id),
                ('location_id', '=', self.env.ref('stock.stock_location_stock').id),
                ('reserved_quantity', '=', 0.0),
            ]
            free_quants = Quant.search(domain, order='id', limit=line.product_uom_qty)
            if len(free_quants) < line.product_uom_qty:
                raise UserError(f"Not enough stock to reserve for {line.product_id.display_name}")
            # Mark each quant as reserved
            for quant in free_quants:
                quant.sudo().write({
                    'reserved_quantity': 1.0,
                    'is_reserved': True,
                })
        # Update sales order status
        order.reserved = True
        order.state = 'reservation'
Key Highlights of this Method
  • Prevents duplicate reservations.
  • Pre-checks stock availability (free_qty).
  • Only reserves storable products (ignores services/consumables).
  • Updates the order state to reservation.

2. Cancel Reservation Method

To release reserved stock when a quotation is cancelled, we override the action_cancel() method:

def action_cancel(self):
    """
    Unreserve stock when a quotation is cancelled.
    """
    for order in self:
        if order.reserved:
            for line in order.order_line:
                reserved_quants = self.env['stock.quant'].search([
                    ('product_id', '=', line.product_id.id),
                    ('is_reserved', '=', True),
                ], limit=line.product_uom_qty)
                for quant in reserved_quants:
                    quant.sudo().write({
                        'reserved_quantity': 0.0,
                        'is_reserved': False,
                    })
            order.reserved = False
    return super().action_cancel()

3. Unreserve Stock on Confirmation

def action_confirm(self):
    """
    Release manual reservations when confirming the Sales Order
    so that Odoo's standard picking workflow can take over.
    """
    for order in self:
        if order.reserved:
            for line in order.order_line:
                reserved_quants = self.env['stock.quant'].search([
                    ('product_id', '=', line.product_id.id),
                    ('is_reserved', '=', True),
                ], limit=line.product_uom_qty)
                for quant in reserved_quants:
                    quant.sudo().write({
                        'reserved_quantity': 0.0,
                        'is_reserved': False,
                    })
            order.reserved = False
    return super().action_confirm()

4. Addition of Reserve Stock Button

Finally, we add a Reserve Stock button visible only on draft quotations:

<record id="view_order_form_reservation" model="ir.ui.view">
    <field name="name">sale.order.form.reservation</field>
    <field name="model">sale.order</field>
    <field name="inherit_id" ref="sale.view_order_form"/>
    <field name="arch" type="xml">
        <header position="inside">
            <button name="action_reserve_stock"
                    type="object"
                    string="Reserve Stock"
                    attrs="{'invisible': [('state', 'not in', ('draft', ‘sent))]}"/>
        </header>
    </field>
</record>

This adds a 'Reserve Stock' button to the Sales Order form view header, visible only when the order is still a quotation (draft).

Workflow in Action

  1. Create Quotation — Add products, quantities, and the customer.
  2. Click "Reserve Stock" — The system checks the free quantity, creates stock moves, and blocks the inventory.
  3. State = Reservation — Sales Order is still a quotation, but stock is already reserved.
  4. Confirm Order — Delivery order is ready with reserved stock.

Benefits of This Approach

  • Prevents overbooking products.
  • Adds a controlled early reservation mechanism.
  • Gives sales teams confidence when quoting customers.
  • Flexible: stock is released automatically if the quotation is cancelled.

Key Takeaways

With these changes, Odoo becomes more flexible for businesses that need early stock allocation. Sales teams can now secure stock at the quotation stage while still keeping a safe rollback mechanism via cancellation.

This customisation bridges the gap between sales flexibility and inventory accuracy, ensuring customers get reliable promises without the risk of overselling.

Found this article useful?

Explore more development guides and solutions from our team.

Check out more posts
Sign in to leave a comment
How to Export Excel Files from Odoo Backend