Stock Orderpoint Safety Stock

stock_orderpoint_safety_stock
REPOSITORY
REPOSITORYOCA/stock-logistics-orderpoint
GIT
GIThttps://github.com/OCA/stock-logistics-orderpoint.git
GIT FOLDER
GIT FOLDERhttps://github.com/OCA/stock-logistics-orderpoint/tree/19.0/stock_orderpoint_safety_stock
VERSION
VERSION 1.0.1
CATEGORY
CATEGORYInventory
LICENSE
LICENSEAGPL-3
APPLICATION
APPLICATIONNo
AUTO-INSTALLABLE
AUTO-INSTALLABLENo
AUTHORS
AUTHORSOdoo Community Association (OCA), Camptocamp
MAINTAINERS
MAINTAINERSOdoo Community Association (OCA), Camptocamp
COMMITTERS
COMMITTERSWeblate, OCA-git-bot, Iván Todorovich, oca-ci
WEBSITE
WEBSITEhttps://github.com/OCA/stock-logistics-orderpoint
LAST TRACKING UPDATE
LAST TRACKING UPDATE2026-07-06 19:40:49
ODOO DEPENDENCIES
ODOO DEPENDENCIES odoo/odoo:
    - stock
    - product
    - base
    - mail
    - base_setup
    - web
    - bus
    - web_tour
    - html_editor
    - uom
    - barcodes_gs1_nomenclature
    - barcodes
    - digest
    - portal
    - http_routing
    - auth_signup
    - resource
PYTHON DEPENDENCIES
PYTHON DEPENDENCIES pyerf
SYSTEM DEPENDENCIES
SYSTEM DEPENDENCIES Not have
DESCRIPTION
DESCRIPTION
This module enhances inventory management by introducing service-level–based, stochastic control using a continuous review replenishment system. Instead of relying solely on fixed min/max stock levels, the module estimates your typical (mean) and variable (variance) daily demand from historical data and factors in lead time. These calculations generate a statistically sound safety stock, which is included in your reorder threshold (the “min”). The “max” is set so your inventory will cover expected demand during the entire replenishment cycle.

Now, you can set replenishment rules (orderpoints) based on the **Cycle Service Level (CSL)**, which reflects the probability of meeting all demand during a replenishment cycle without running out of stock. This provides a practical alternative to manually setting minimum and maximum stock thresholds.

With CSL enabled, safety stock is automatically calculated using your historical daily sales data (both average and standard deviation), gathered over a period you define, plus the orderpoint’s lead time. The system then keeps your reordering rule's min and max levels up-to-date.

Prefer manual control? You can always switch back to the “Manual” mode to specify min and max directly.

## Theory

The backbone of this approach is the Cycle Service Level (CSL), a widely used supply chain metric:

- **CSL Definition:** The chance that your inventory will fully cover demand during a restocking cycle.
- **Example:** A CSL of 95% means that only 5% of cycles will risk a stockout.

This system assumes demand is random (not fixed), so it uses statistical methods:

- **Average daily demand** (μ)
- **Standard deviation of daily demand** (σ)

Because demand can fluctuate during the lead time, safety stock acts as a buffer to reduce the risk of running out of stock.

**Safety stock formula:**

`safety_stock = σ_L × z × g`

Where:

- **σ_L:** Standard deviation of demand over the lead time
- **z:** Z-score for your desired CSL (e.g., 1.65 for 95% CSL)
- **g:** Growth factor (optional, lets you add a margin)

**Three zones are needed to define how min and max are derived:**

- **Red zone = safety stock:**
  - This zone should never be touched. It acts as the buffer for unexpected variation.
  - Refer to the safety stock formula above.
- **Yellow zone = expected demand during the lead time:**
  - This zone represents the expected stock consumption from the moment you click on replenish, until the moment you receive your purchase order.
  - Formula: `average daily demand × lead time in days`
- **Green zone = expected demand during the cycle:**
  - Represents the stock consumption during the desired reordering cycle (the time between two replenishments)
  - Formula: `average daily demand × cycle days`

Where:

- **lead time:** The time it takes to receive the order.
- **cycle days:** The desired number of days between orders.

From these three zones, the min and max quantities are derived as follows:
- **Minimum (min):** Red + Yellow
- **Maximum (max):** Red + Yellow + Green

**Why does it work?**

- Odoo triggers replenishment whenever stock falls below the min, which should be enough to cover variance (safety stock) and lead time demand.
- The max level is set high enough to cover all expected demand until the next restock, plus a buffer to cover the desired cycle days.

This makes inventory management both more data-driven and easier to maintain.

![cycle-graph](../static/description/cycle-graph.png)

Code Analysis

Views touched (6)
XML IDNameModelTypeStatus
res_config_settings_view_form res.config.settings xpath Inherits stock.res_config_settings_view_form
stock_reorder_report_search stock.warehouse.orderpoint field Inherits stock.stock_reorder_report_search
view_stock_cycle_service_level_list stock.cycle.service.level list New
view_stock_replenishment_info stock.replenishment.info xpath Inherits stock.view_stock_replenishment_info
view_stock_warehouse_orderpoint_tree_editable stock.warehouse.orderpoint field Inherits stock.view_warehouse_orderpoint_tree_editable
view_warehouse_orderpoint_form stock.warehouse.orderpoint field Inherits stock.view_warehouse_orderpoint_form
Models touched (6)

New fields (0)

No new fields.

Public methods (0)

No public methods.

New fields (1)
  • demand_history_days Integer
    default=365 help='The number of days in the past to use to compute the safety stock.'
Public methods (0)

No public methods.

New fields (1)
  • demand_history_days Integer
    readonly=False related='company_id.demand_history_days'
Public methods (0)

No public methods.

New fields (4)
  • csl Float
    digits=(2, 2) help='The probability that demand during the replenishment lead time is fully covered without a stockout.\nThis value defines how reliable the stock should be during each replenishment cycle. A higher target increases safety stock to reduce the risk of stockouts; a lower target reduces inventory at the cost of more frequent shortages.\nTypical values range from 0.90 to 0.99 (e.g., 0.95 = 95% of cycles without stockouts).' string='Cycle Service Level'
  • orderpoint_count Integer
    compute='_compute_orderpoint_count' string='Orderpoints Count'
  • orderpoint_ids One2many → stock.warehouse.orderpoint
    string='Orderpoints' args: 'stock.warehouse.orderpoint', 'cycle_service_level_id'
  • z_score Float
    compute='_compute_z_score' digits=(16, 4) help='Statistical factor derived from the target cycle service level.\n\nThe z-score represents how many standard deviations of demand variability are covered by the safety stock.\nA higher z-score corresponds to a higher service level and results in more safety stock.' store=True string='Z-Score'
Public methods (1)
  • action_open_orderpoints(self)

New fields (10)
  • cycle_days Integer
    readonly=False related='orderpoint_id.cycle_days'
  • cycle_service_level_id Many2one
    readonly=False related='orderpoint_id.cycle_service_level_id'
  • demand_avg_qty Float
    related='orderpoint_id.demand_avg_qty'
  • demand_history_days Integer
    related='orderpoint_id.demand_history_days'
  • demand_lt_std_dev Float
    related='orderpoint_id.demand_lt_std_dev'
  • demand_std_dev Float
    related='orderpoint_id.demand_std_dev'
  • growth_factor Float
    readonly=False related='orderpoint_id.growth_factor'
  • safety_stock Float
    related='orderpoint_id.safety_stock'
  • safety_stock_method Selection
    readonly=False related='orderpoint_id.safety_stock_method'
  • z_score Float
    depends=['orderpoint_id.cycle_service_level_id'] related='orderpoint_id.z_score'
Public methods (0)

No public methods.

New fields (12)
  • csl Float
    related='cycle_service_level_id.csl' string='Cycle Service Level Target'
  • cycle_days Integer
    help='The desired number of days between orders.\nUsed to size the gap between the min and max quantities, to cover the expected demand during the desired reordering cycle.\n\nproduct_max_qty = product_min_qty + (demand_avg_qty * cycle_days)'
  • cycle_service_level_id Many2one → stock.cycle.service.level
    ondelete='restrict' string='Cycle Service Level' args: 'stock.cycle.service.level'
  • demand_avg_qty Float
    compute='_compute_daily_demand' digits='Product Unit' help='The average daily outgoing quantity on this warehouse.' string='Average Daily Demand'
  • demand_history_days Integer
    related='company_id.demand_history_days' string='Demand History Days'
  • demand_lt_std_dev Float
    compute='_compute_demand_lt_std_dev' digits='Product Unit' help='The standard deviation of the daily outgoing quantity on this warehouse over the lead time.' string='Standard Deviation of Daily Demand over Lead Time'
  • demand_std_dev Float
    compute='_compute_daily_demand' digits='Product Unit' help='The standard deviation of the daily outgoing quantity on this warehouse.' string='Standard Deviation of Daily Demand'
  • growth_factor Float
    default=0.0 digits=(2, 2) help='A multiplier to apply to the safety stock and the resulting min and max quantities. Used to account for expected growth or decrease in demand.'
  • safety_stock Float
    compute='_compute_safety_stock' digits='Product Unit' help='The safety stock is the amount of stock to keep on this warehouse to cover for the variability in demand during the lead time.\nIt is computed as the product of the standard deviation of the daily outgoing quantity over the lead time, the z-score (statistical factor derived from the target cycle service level) and the growth factor.\n\nsafety_stock = demand_lt_std_dev * z_score * (1.0 + growth_factor)'
  • safety_stock_method Selection
    default='manual' help="The method to use to set the product's min and max quantities.\n* Manual: The product's min and max quantities are set manually.\n* Cycle Service Level: The product's min and max quantities are computed based on the target cycle service level and the growth factor.\n" selection=[('manual', 'Manual'), ('csl', 'Cycle Service Level')]
  • safety_stock_update_date Datetime
    readonly=True string='Last modification date of Min and Max quantities from Safety Stock'
  • z_score Float
    related='cycle_service_level_id.z_score'
Public methods (1)
  • action_apply_safety_stock(self)
    Apply the safety stock to the orderpoint min and max quantities