Odoo MTO 和智能MTO 完全解读:从源码到实战

本文将深入剖析 Odoo 库存管理中的 MTO(按订单生产)和智能MTO(MTS else MTO)机制,结合源码分析和实际案例,帮助你完全掌握这套核心的采购补货策略。


目录

  1. 核心概念
  2. 数据库模型设计
  3. [MTO 工作流程详解](#MTO 工作流程详解)
  4. [智能MTO 核心算法](#智能MTO 核心算法)
  5. 规则优先级机制
  6. [采购 vs 生产决策](#采购 vs 生产决策)
  7. 实战案例
  8. 配置指南
  9. 调试技巧

一、核心概念

1.1 什么是MTO?

MTO (Make to Order - 按订单生产/采购) 是一种库存策略:当产品需求产生时,不从现有库存中取货,而是触发新的采购订单或生产订单。

适用场景:

  • 定制化产品(如定制家具)
  • 高价值低周转产品(如特种设备)
  • JIT(准时制)生产模式

1.2 什么是智能MTO?

智能MTO (MTS else MTO) 是一种混合策略:

  • 优先从库存取货 (Make to Stock)
  • 库存不足时触发采购/生产 (Make to Order)

适用场景:

  • 常规产品(如办公用品)
  • 需要平衡库存成本和缺货风险的产品
  • 波动性需求的产品

1.3 三种采购方法对比

采购方法 字段值 库存检查 采购触发 适用场景
从库存取货 make_to_stock 从源库位直接取货 库存充足场景
按订单采购 make_to_order 总是触发采购/生产 定制化产品
智能MTO mts_else_mto 库存不足时触发 常规产品优化

二、数据库模型设计

2.1 核心模型:Stock Rule (库存规则)

python 复制代码
class StockRule(models.Model):
    _name = 'stock.rule'
    _description = "Stock Rule"
    
    # 规则名称
    name = fields.Char('Name', required=True, translate=True)
    
    # 是否激活
    active = fields.Boolean('Active', default=True)
    
    # 动作类型:拉取、推送、采购、生产
    action = fields.Selection([
        ('pull', 'Pull From'),      # 从源库位拉取
        ('push', 'Push To'),         # 推送到目标库位
        ('pull_push', 'Pull & Push') # 拉取并推送
    ], string='Action', default='pull', required=True)
    
    # 规则序号(越小越优先)
    sequence = fields.Integer('Sequence', default=20)
    
    # 所属路线
    route_id = fields.Many2one('stock.route', 'Route', required=True, ondelete='cascade')
    
    # 路线序号(从route继承,用于排序)
    route_sequence = fields.Integer('Route Sequence', related='route_id.sequence', store=True)
    
    # 采购方法(核心字段)
    procure_method = fields.Selection([
        ('make_to_stock', 'Take From Stock'),           # 从库存取货
        ('make_to_order', 'Trigger Another Rule'),      # 触发另一条规则(MTO)
        ('mts_else_mto', 'Take From Stock, if unavailable, Trigger Another Rule')  # 智能MTO
    ], string='Supply Method', default='make_to_stock', required=True)
    
    # 源库位
    location_src_id = fields.Many2one('stock.location', 'Source Location')
    
    # 目标库位
    location_dest_id = fields.Many2one('stock.location', 'Destination Location', required=True)
    
    # 操作类型
    picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', required=True)
    
    # 提前期(天)
    delay = fields.Integer('Lead Time', default=0)

2.2 采购模块扩展

采购模块为 action 字段添加了 buy 选项:

python 复制代码
class StockRule(models.Model):
    _inherit = 'stock.rule'
    
    action = fields.Selection(selection_add=[
        ('buy', 'Buy')  # 采购动作
    ], ondelete={'buy': 'cascade'})

2.3 生产模块扩展

生产模块为 action 字段添加了 manufacture 选项:

python 复制代码
class StockRule(models.Model):
    _inherit = 'stock.rule'
    
    action = fields.Selection(selection_add=[
        ('manufacture', 'Manufacture')  # 生产动作
    ], ondelete={'manufacture': 'cascade'})

三、MTO 工作流程详解

3.1 流程概览

复制代码
销售订单确认
    ↓
创建出库移动(Stock Move)
    ↓
移动确认(_action_confirm)
    ↓
检查 procure_method
    ├─ make_to_stock → 直接预留库存
    ├─ make_to_order → 创建采购请求
    └─ mts_else_mto → 计算库存缺口,创建采购请求
    ↓
查找匹配规则(_get_rule)
    ↓
执行规则动作
    ├─ action='buy' → _run_buy() → 创建采购订单
    ├─ action='manufacture' → _run_manufacture() → 创建生产订单
    └─ action='pull' → 创建上游移动

3.2 移动确认流程源码

python 复制代码
def _action_confirm(self, merge=True, merge_into=False, create_proc=True):
    """确认库存移动"""
    move_to_confirm = set()      # 需要确认的移动
    move_create_proc = set()     # 需要创建采购请求的移动
    
    for move in self:
        # 处理 make_to_order 移动
        if move.procure_method == 'make_to_order' and create_proc:
            move_to_confirm.add(move.id)
            move_create_proc.add(move.id)
        
        # 处理 mts_else_mto 移动
        elif move.rule_id and move.rule_id.procure_method == 'mts_else_mto':
            move_to_confirm.add(move.id)
            if create_proc:
                move_create_proc.add(move.id)
        
        # 普通移动
        else:
            move_to_confirm.add(move.id)
        
        # 检查是否需要立即分配库存
        if move._should_be_assigned():
            key = (frozenset(move.reference_ids.ids), move.location_id.id, move.location_dest_id.id)
            to_assign[key].add(move.id)
    
    # 为 MTO 移动创建采购请求
    procurement_requests = []
    move_create_proc = self.browse(move_create_proc)
    
    # 关键:计算实际需要采购的数量
    quantities = move_create_proc._prepare_procurement_qty()
    
    for move, quantity in zip(move_create_proc, quantities):
        values = move._prepare_procurement_values()
        origin = move._prepare_procurement_origin()
        
        # 创建采购请求对象
        procurement_requests.append(self.env['stock.rule'].Procurement(
            move.product_id,           # 产品
            quantity,                  # 数量
            move.product_uom,          # 单位
            move.location_id,          # 源库位
            move.product_id.name,      # 名称
            origin,                    # 来源
            move.company_id,           # 公司
            values                     # 额外参数
        ))
    
    # 执行采购请求
    if procurement_requests:
        self.env['stock.rule'].run(procurement_requests)
    
    return moves

3.3 规则执行入口

python 复制代码
@api.model
def run(self, procurements, raise_user_error=True):
    """执行采购请求"""
    actions_to_run = defaultdict(list)
    procurement_errors = []
    
    for procurement in procurements:
        # 设置默认值
        procurement.values.setdefault('company_id', procurement.location_id.company_id)
        procurement.values.setdefault('priority', '0')
        procurement.values.setdefault('date_planned', fields.Datetime.now())
        
        # 查找匹配的规则
        rule = self._get_rule(procurement.product_id, procurement.location_id, procurement.values)
        
        if not rule:
            # 没有找到规则,报错
            error = _('未找到规则来补货产品 "%(product)s" 到库位 "%(location)s"',
                product=procurement.product_id.display_name, 
                location=procurement.location_id.display_name)
            procurement_errors.append((procurement, error))
        else:
            # 按规则动作分组
            action = 'pull' if rule.action in ('pull', 'pull_push') else rule.action
            actions_to_run[action].append((procurement, rule))
    
    # 报告错误
    if procurement_errors:
        raise ProcurementException(procurement_errors)
    
    # 执行各类动作
    for action, procurements in actions_to_run.items():
        if hasattr(self, '_run_%s' % action):
            # 调用对应的方法:_run_buy、_run_manufacture、_run_pull 等
            getattr(self, '_run_%s' % action)(procurements)
    
    return True

四、智能MTO 核心算法

4.1 数量计算逻辑

智能MTO的核心在于 _prepare_procurement_qty() 方法,它决定实际需要采购多少数量:

python 复制代码
def _prepare_procurement_qty(self):
    """计算采购数量(智能MTO核心算法)"""
    quantities = []
    mtso_products_by_locations = defaultdict(list)
    mtso_moves = set()
    
    # 步骤1: 识别所有智能MTO移动
    for move in self:
        if move.rule_id and move.rule_id.procure_method == 'mts_else_mto':
            mtso_moves.add(move.id)
            mtso_products_by_locations[move.location_id].append(move.product_id.id)
    
    # 步骤2: 批量查询预测库存(性能优化)
    forecasted_qties_by_loc = {}
    for location, product_ids in mtso_products_by_locations.items():
        if location.should_bypass_reservation():
            continue
        
        # 带上库位上下文,查询可用数量
        products = self.env['product.product'].browse(product_ids).with_context(location=location.id)
        forecasted_qties_by_loc[location] = {
            product.id: product.free_qty  # free_qty = 现有量 - 已预留量
            for product in products
        }
    
    # 步骤3: 计算每个移动的实际采购数量
    for move in self:
        # 非智能MTO或负数数量,采购全部
        if move.id not in mtso_moves or move.product_qty <= 0:
            quantities.append(move.product_uom_qty)
            continue
        
        # 跳过预留检查的库位
        if move._should_bypass_reservation():
            quantities.append(move.product_uom_qty)
            continue
        
        # 核心计算:需求量 - 可用库存 = 采购量
        free_qty = max(forecasted_qties_by_loc[move.location_id][move.product_id.id], 0)
        quantity = max(move.product_qty - free_qty, 0)  # 只采购缺口部分
        
        # 单位转换
        product_uom_qty = move.product_id.uom_id._compute_quantity(
            quantity, 
            move.product_uom, 
            rounding_method='HALF-UP'
        )
        quantities.append(product_uom_qty)
        
        # 步骤4: 更新预测库存(处理多个移动时的累计效应)
        forecasted_qties_by_loc[move.location_id][move.product_id.id] -= min(move.product_qty, free_qty)
    
    return quantities

4.2 算法示例

场景: 三个销售订单同时确认,产品当前库存 100 件

订单 需求量 可用库存 计算过程 采购量
SO001 50 100 max(50-100, 0) = 0 0 (从库存取)
SO002 60 50 max(60-50, 0) = 10 10 (部分采购)
SO003 30 -10 max(30-(-10), 0) = 40 40 (全部采购)

总采购量: 0 + 10 + 40 = 50 件


五、规则优先级机制

5.1 两级排序系统

Odoo 使用两个字段控制规则优先级:

  1. route_sequence (路线序号) - 第一优先级
  2. sequence (规则序号) - 第二优先级

规则:数值越小,优先级越高

5.2 规则查找核心代码

python 复制代码
def _search_rule(self, route_ids, packaging_uom_id, product_id, warehouse_id, domain):
    """按优先级查找规则"""
    Rule = self.env['stock.rule']
    res = self.env['stock.rule']
    domain = Domain(domain)
    
    if warehouse_id:
        domain &= Domain('warehouse_id', 'in', [False, warehouse_id.id])
    
    # 优先级1: 采购组指定的路线
    if route_ids:
        res = Rule.search(
            Domain('route_id', 'in', route_ids.ids) & domain, 
            order='route_sequence, sequence',  # 关键:两级排序
            limit=1
        )
    
    # 优先级2: 包装类型路线
    if not res and packaging_uom_id:
        packaging_routes = packaging_uom_id.package_type_id.route_ids
        if packaging_routes:
            res = Rule.search(
                Domain('route_id', 'in', packaging_routes.ids) & domain, 
                order='route_sequence, sequence', 
                limit=1
            )
    
    # 优先级3: 产品自身的路线(最常用)
    if not res:
        product_routes = product_id.route_ids | product_id.categ_id.total_route_ids
        if product_routes:
            res = Rule.search(
                Domain('route_id', 'in', product_routes.ids) & domain, 
                order='route_sequence, sequence', 
                limit=1
            )
    
    # 优先级4: 仓库默认路线
    if not res and warehouse_id:
        warehouse_routes = warehouse_id.route_ids
        if warehouse_routes:
            res = Rule.search(
                Domain('route_id', 'in', warehouse_routes.ids) & domain, 
                order='route_sequence, sequence', 
                limit=1
            )
    
    return res

5.3 规则提取算法

当产品配置了多个路线时,系统使用特殊的排序算法:

python 复制代码
def extract_rule(rule_dict, route_ids, warehouse_id, location_dest_id):
    """从规则字典中提取最优规则"""
    rule = self.env['stock.rule']
    
    # 路线排序关键逻辑
    for route_id in sorted(route_ids, key=lambda r: (
        r not in product_id.route_ids,  # 产品直接关联的路线优先
        r.sequence                       # 然后按路线序号排序
    )):
        sub_dict = rule_dict.get((location_dest_id.id, route_id.id))
        if not sub_dict:
            continue
        
        if not warehouse_id:
            rule = sub_dict[next(iter(sub_dict))]
        else:
            # 优先匹配仓库特定规则
            rule = sub_dict.get(warehouse_id.id)
            # 回退到通用规则
            rule = rule or sub_dict[False]
        
        if rule:
            break
    
    return rule

六、采购 vs 生产决策

6.1 决策流程图

复制代码
需求产生 → 查找规则 → 检查 rule.action
                         ↓
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
    action='buy'   action='manufacture'  action='pull'
        ↓                ↓                ↓
    _run_buy()    _run_manufacture()   _run_pull()
        ↓                ↓                ↓
    创建采购订单      创建生产订单       创建库存移动

6.2 采购执行源码

python 复制代码
@api.model
def _run_buy(self, procurements):
    """执行采购动作"""
    procurements_by_po_domain = defaultdict(list)
    errors = []
    
    for procurement, rule in procurements:
        company_id = rule.company_id or procurement.company_id
        
        # 步骤1: 获取供应商
        supplier = rule._get_matching_supplier(
            procurement.product_id, 
            procurement.product_qty, 
            procurement.product_uom,
            company_id, 
            procurement.values
        )
        
        if not supplier:
            # 没有供应商处理
            if self.env.context.get('from_orderpoint'):
                msg = _('没有供应商可以补货产品 %s', procurement.product_id.display_name)
                errors.append((procurement, msg))
            else:
                # 回退到MTS
                moves = procurement.values.get('move_dest_ids') or self.env['stock.move']
                if moves.propagate_cancel:
                    moves._action_cancel()
                moves.procure_method = 'make_to_stock'
                self._notify_responsible(procurement)
                continue
        
        partner = supplier.partner_id
        procurement.values['supplier'] = supplier
        procurement.values['propagate_cancel'] = rule.propagate_cancel
        
        # 步骤2: 按采购订单域分组
        domain = rule._make_po_get_domain(company_id, procurement.values, partner)
        procurements_by_po_domain[domain].append((procurement, rule))
    
    if errors:
        raise ProcurementException(errors)
    
    # 步骤3: 为每个采购订单域创建或更新采购订单
    for domain, procurements_rules in procurements_by_po_domain.items():
        procurements, rules = zip(*procurements_rules)
        origins = set([p.origin for p in procurements if p.origin])
        
        # 查找现有采购订单
        po = self.env['purchase.order'].sudo().search([dom for dom in domain], limit=1)
        company_id = rules[0].company_id or procurements[0].company_id
        
        if not po:
            # 创建新采购订单
            positive_values = [p.values for p in procurements if p.product_qty >= 0]
            if positive_values:
                vals = rules[0]._prepare_purchase_order(company_id, origins, positive_values)
                po = self.env['purchase.order'].with_user(SUPERUSER_ID).create(vals)
        else:
            # 更新现有采购订单来源
            reference_ids = set()
            for procurement in procurements:
                reference_ids |= set(procurement.values.get('reference_ids', self.env['stock.reference']).ids)
            po.reference_ids = [Command.link(ref_id) for ref_id in reference_ids]
            
            if po.origin:
                missing_origins = origins - set(po.origin.split(', '))
                if missing_origins:
                    po.write({'origin': po.origin + ', ' + ', '.join(missing_origins)})
            else:
                po.write({'origin': ', '.join(origins)})
        
        # 步骤4: 创建或更新采购订单行
        # ... (代码略,主要是合并采购行或创建新行)

6.3 生产执行源码

python 复制代码
@api.model
def _run_manufacture(self, procurements):
    """执行生产动作"""
    new_productions_values_by_company = defaultdict(lambda: defaultdict(list))
    
    for procurement, rule in procurements:
        if procurement.product_qty <= 0:
            continue  # 负数数量不创建生产订单
        
        # 步骤1: 获取BOM(物料清单)
        bom = rule._get_matching_bom(
            procurement.product_id, 
            procurement.company_id, 
            procurement.values
        )
        
        # 步骤2: 查找现有生产订单
        mo = self.env['mrp.production']
        if procurement.origin != 'MPS':
            domain = rule._make_mo_get_domain(procurement, bom)
            mo = self.env['mrp.production'].sudo().search(domain, limit=1)
        
        is_batch_size = bom and bom.enable_batch_size
        
        if not mo or is_batch_size:
            # 步骤3: 创建新生产订单(支持批量生产)
            procurement_qty = procurement.product_qty
            batch_size = bom.product_uom_id._compute_quantity(
                bom.batch_size, procurement.product_uom
            ) if is_batch_size else procurement_qty
            
            vals = rule._prepare_mo_vals(*procurement, bom)
            
            # 按批量大小拆分生产订单
            while procurement_qty > 0:
                new_productions_values_by_company[procurement.company_id.id]['values'].append({
                    **vals,
                    'product_qty': procurement.product_uom._compute_quantity(
                        batch_size, bom.product_uom_id
                    ) if bom else procurement_qty,
                })
                new_productions_values_by_company[procurement.company_id.id]['procurements'].append(procurement)
                procurement_qty -= batch_size
        else:
            # 步骤4: 更新现有生产订单数量
            procurement_product_uom_qty = procurement.product_uom._compute_quantity(
                procurement.product_qty, 
                procurement.product_id.uom_id
            )
            self.env['change.production.qty'].sudo().with_context(skip_activity=True).create({
                'mo_id': mo.id,
                'product_qty': mo.product_uom_qty + procurement_product_uom_qty,
            }).change_prod_qty()
    
    # 步骤5: 批量创建生产订单
    for company_id in new_productions_values_by_company:
        productions_vals_list = new_productions_values_by_company[company_id]['values']
        productions = self.env['mrp.production'].with_user(SUPERUSER_ID).create(productions_vals_list)
        
        # 自动确认生产订单
        for mo in productions:
            if self._should_auto_confirm_procurement_mo(mo):
                mo.action_confirm()
        
        productions._post_run_manufacture(new_productions_values_by_company[company_id]['procurements'])
    
    return True

七、实战案例

案例1: 电商公司的库存优化

背景:

  • 公司: 电商零售企业
  • 产品: 办公用品(笔、本子等)
  • 问题: 库存积压严重,但偶尔缺货

解决方案:使用智能MTO

步骤1: 产品配置
python 复制代码
# 产品: 签字笔
product = self.env['product.product'].browse(123)

# 配置路线
mto_route = self.env.ref('stock.route_warehouse0_mto')  # MTO路线
buy_route = self.env.ref('purchase_stock.route_warehouse0_buy')  # 采购路线

product.write({
    'route_ids': [(6, 0, [mto_route.id, buy_route.id])]
})

# 关键:将MTO路线的规则改为智能MTO
mto_rule = mto_route.rule_ids.filtered(lambda r: r.location_dest_id == warehouse.lot_stock_id)
mto_rule.write({
    'procure_method': 'mts_else_mto'  # 智能MTO
})
步骤2: 实际运行

场景A: 库存充足

复制代码
当前库存: 100 支
销售订单: 50 支
结果: 直接从库存发货,不触发采购

场景B: 库存不足

复制代码
当前库存: 30 支
销售订单: 50 支
计算: max(50 - 30, 0) = 20 支
结果: 从库存发30支,自动创建采购订单补20支

效果:

  • 库存周转率提升 40%
  • 缺货率下降 80%
  • 库存成本降低 25%

案例2: 制造企业的按单生产

背景:

  • 公司: 定制家具制造商
  • 产品: 定制书柜
  • 需求: 每个订单配置不同

解决方案:使用纯MTO + 生产

步骤1: 产品配置
python 复制代码
# 产品: 定制书柜
product = self.env['product.product'].create({
    'name': '定制书柜',
    'type': 'product',
    'is_storable': True,
})

# 创建BOM(物料清单)
bom = self.env['mrp.bom'].create({
    'product_tmpl_id': product.product_tmpl_id.id,
    'product_id': product.id,
    'bom_line_ids': [
        (0, 0, {'product_id': wood_board.id, 'product_qty': 5}),
        (0, 0, {'product_id': screw.id, 'product_qty': 20}),
    ]
})

# 配置路线
mto_route = self.env.ref('stock.route_warehouse0_mto')
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture')

product.write({
    'route_ids': [(6, 0, [mto_route.id, manufacture_route.id])]
})

# 确保MTO规则为纯MTO(不检查库存)
mto_rule = mto_route.rule_ids.filtered(lambda r: r.location_dest_id == warehouse.lot_stock_id)
mto_rule.write({
    'procure_method': 'make_to_order'  # 纯MTO
})
步骤2: 订单流程
复制代码
客户下单 → 销售订单确认
    ↓
创建出库移动(procure_method='make_to_order')
    ↓
查找规则(找到 action='manufacture' 的规则)
    ↓
调用 _run_manufacture()
    ↓
创建生产订单 MO001
    ↓
生产订单确认 → 生成原料需求
    ↓
原料也触发MTO采购(木板、螺丝)

效果:

  • 零库存生产
  • 100% 定制化
  • 客户满意度提升

案例3: 混合策略的高科技公司

背景:

  • 公司: 电子设备制造商
  • 产品: 平板电脑
  • 情况: 标准型号自产,特殊配置外购

解决方案:双路线策略

配置方案
python 复制代码
# 产品: 平板电脑
product = self.env['product.product'].browse(456)

# 配置两条路线
mto_route = self.env.ref('stock.route_warehouse0_mto')
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture')
buy_route = self.env.ref('purchase_stock.route_warehouse0_buy')

product.write({
    'route_ids': [(6, 0, [mto_route.id, manufacture_route.id, buy_route.id])]
})

# 关键:通过route_sequence控制优先级
manufacture_route.write({'sequence': 10})  # 生产优先
buy_route.write({'sequence': 20})          # 采购次之
决策逻辑
python 复制代码
# 系统查找规则时:
# 1. 先找到 manufacture_route (sequence=10)
# 2. 检查是否有BOM
#    - 有BOM → 创建生产订单
#    - 无BOM → 规则不适用,继续查找
# 3. 找到 buy_route (sequence=20)
# 4. 创建采购订单

场景演示:

python 复制代码
# 标准型号(有BOM)
product_standard.bom_ids  # 存在BOM
# 销售订单确认 → 创建生产订单 ✓

# 特殊配置(无BOM)
product_special.bom_ids  # 无BOM
# 销售订单确认 → 跳过生产规则 → 创建采购订单 ✓

八、配置指南

8.1 激活MTO功能

路径: 库存 → 配置 → 设置

勾选:

  • 多步路线 (Multi-Step Routes)
  • 按订单补货 (MTO) (Replenish on Order)

8.2 产品配置

方法1: 通过产品表单
  1. 进入 产品 → 产品 → 选择产品
  2. 切换到 库存 选项卡
  3. 路线 部分勾选:
    • 补货按订单 (MTO)
    • 采购制造 (根据需求)
方法2: 通过代码
python 复制代码
# 配置产品为MTO + 采购
product = self.env['product.product'].browse(product_id)

mto_route = self.env.ref('stock.route_warehouse0_mto')
buy_route = self.env.ref('purchase_stock.route_warehouse0_buy')

product.write({
    'route_ids': [(6, 0, [mto_route.id, buy_route.id])]
})

8.3 规则配置

修改规则为智能MTO
  1. 进入 库存 → 配置 → 路线
  2. 选择 Replenish on Order (MTO) 路线
  3. 切换到 规则 选项卡
  4. 编辑规则,修改 Supply Method :
    • Take From Stock = 普通MTS
    • Trigger Another Rule = 纯MTO
    • Take From Stock, if unavailable, Trigger Another Rule = 智能MTO
调整规则优先级
python 复制代码
# 设置路线序号
route_manufacture.write({'sequence': 10})  # 生产优先
route_buy.write({'sequence': 20})          # 采购次之

# 设置规则序号(同一路线内)
rule1.write({'sequence': 1})  # 优先
rule2.write({'sequence': 2})  # 次之

8.4 仓库配置

路径: 库存 → 配置 → 仓库

  • 接收步骤: 一步/两步/三步
  • 发货步骤: 一步/拣货+包装+发货
  • 制造补货: 激活后自动创建制造MTO规则

九、调试技巧

9.1 查看产品会触发哪个规则

python 复制代码
# 在 Odoo shell 中执行
product = self.env['product.product'].browse(123)
location = self.env.ref('stock.stock_location_stock')  # 库存库位
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)

rule = self.env['stock.rule']._get_rule(product, location, {
    'warehouse_id': warehouse,
})

print(f"匹配规则: {rule.name}")
print(f"规则动作: {rule.action}")
print(f"采购方法: {rule.procure_method}")
print(f"所属路线: {rule.route_id.name} (序号={rule.route_sequence})")
print(f"规则序号: {rule.sequence}")
print(f"源库位: {rule.location_src_id.name}")
print(f"目标库位: {rule.location_dest_id.name}")

9.2 模拟智能MTO计算

python 复制代码
# 模拟智能MTO的库存检查
product = self.env['product.product'].browse(123)
location = self.env.ref('stock.stock_location_stock')

# 查询可用数量
product_with_location = product.with_context(location=location.id)
free_qty = product_with_location.free_qty

demand_qty = 100  # 需求数量
procurement_qty = max(demand_qty - free_qty, 0)

print(f"可用库存: {free_qty}")
print(f"需求数量: {demand_qty}")
print(f"需采购量: {procurement_qty}")

9.3 查看移动的采购方法

python 复制代码
# 查看销售订单关联的库存移动
sale_order = self.env['sale.order'].browse(456)
moves = sale_order.picking_ids.move_ids

for move in moves:
    print(f"产品: {move.product_id.name}")
    print(f"采购方法: {move.procure_method}")
    print(f"关联规则: {move.rule_id.name if move.rule_id else '无'}")
    print(f"状态: {move.state}")
    print("---")

9.4 启用日志调试

odoo.conf 中添加:

ini 复制代码
[options]
log_level = debug
log_handler = odoo.addons.stock.models.stock_rule:DEBUG

这会输出规则查找和执行的详细日志。

9.5 使用开发者模式查看规则

  1. 激活 开发者模式 (设置 → 激活开发者模式)
  2. 进入 库存 → 产品 → 选择产品
  3. 点击 动作 → 查看路线
  4. 可以看到产品的所有路线和规则可视化图

十、常见问题与解决方案

问题1: MTO不触发采购/生产

原因分析:

python 复制代码
# 检查产品配置
product.route_ids  # 是否包含MTO路线?
product.seller_ids  # 采购需要供应商
product.bom_ids    # 生产需要BOM

解决方案:

  1. 确保产品勾选了MTO路线
  2. 采购模式需配置供应商
  3. 生产模式需创建BOM

问题2: 智能MTO总是触发采购

原因: 库存计算可能包含了预留数量

检查:

python 复制代码
product.qty_available  # 现有量
product.virtual_available  # 预测量
product.free_qty  # 可用量(用于智能MTO)

解决方案: 确保使用 free_qty 而非 qty_available

问题3: 规则优先级不符合预期

调试步骤:

python 复制代码
# 查看产品的所有规则
product = self.env['product.product'].browse(123)
location = self.env.ref('stock.stock_location_customers')

rules = product._get_rules_from_location(location)
for rule in rules:
    print(f"{rule.route_id.sequence}.{rule.sequence} - {rule.name} ({rule.action})")

解决方案: 调整 route.sequencerule.sequence

问题4: MTO采购合并到错误的订单

原因: 采购订单域匹配逻辑

查看域:

python 复制代码
# 在 _run_buy 中打印
domain = rule._make_po_get_domain(company_id, procurement.values, partner)
print(domain)

解决方案: 调整采购组或供应��配置


十一、性能优化建议

11.1 批量处理

智能MTO已经做了批量查询优化:

python 复制代码
# 好的做法:批量查询库存
products = self.env['product.product'].browse(product_ids).with_context(location=location.id)
forecasted_qties = {product.id: product.free_qty for product in products}

# 差的做法:逐个查询
for product_id in product_ids:
    product = self.env['product.product'].browse(product_id).with_context(location=location.id)
    qty = product.free_qty  # 每次都会查询数据库

11.2 规则缓存

对于相同产品和库位,规则查找结果可以缓存:

python 复制代码
# 在 _compute_rules 中使用缓存
rules_cache = {}
for orderpoint in orderpoints:
    cache_key = (orderpoint.location_id, orderpoint.route_id, all_product_routes)
    rule_ids = rules_cache.get(cache_key) or orderpoint.product_id._get_rules_from_location(
        orderpoint.location_id, route_ids=orderpoint.route_id
    )
    rules_cache[cache_key] = rule_ids

11.3 索引优化

确保关键字段有索引:

python 复制代码
# stock_rule 表的索引
route_id = fields.Many2one(..., index=True)  # 路线索引
location_dest_id = fields.Many2one(..., index=True)  # 目标库位索引
route_sequence = fields.Integer(..., store=True)  # 排序字段存储

十二、总结

核心要点回顾

  1. MTO vs 智能MTO

    • MTO: 总是触发采购/生产
    • 智能MTO: 优先使用库存,不足时才触发
  2. 规则优先级

    • 两级排序: route_sequencesequence
    • 数值越小越优先
  3. 采购/生产决策

    • 通过 rule.action 字段决定
    • buy = 采购, manufacture = 生产
  4. 智能MTO算法

    • 采购量 = max(需求量 - 可用库存, 0)
    • 批量查询库存优化性能

适用场景总结

策略 适用产品 优势 劣势
纯MTS 快消品 响应快 库存成本高
纯MTO 定制品 零库存 交期长
智能MTO 常规品 平衡库存和缺货 配置复杂

最佳实践

  1. 分层配置

    • A类产品: 智能MTO
    • B类产品: 纯MTS
    • C类产品: 纯MTO
  2. 定期审查

    • 每季度检查规则有效性
    • 根据周转率调整策略
  3. 监控指标

    • 库存周转率
    • 缺货率
    • 采购订单数量
    • 平均交付时间

相关资源

官方文档

源码文件


希望这篇文章能帮助你深入理解 Odoo 的 MTO 和智能MTO 机制!如有疑问,欢迎讨论。

相关推荐
odoo中国17 小时前
Odoo 19 中升级(迁移)脚本的使用方法
odoo·odoo19·odoo技术·升级脚本·迁移脚本
odoo中国2 天前
Odoo 19 安全完整解析:多层防护守护企业核心数据
安全·odoo·数据备份·数据保护·用户权限·odoo19·用户访问规则
odoo中国4 天前
Odoo 19 功能性报表解析:如何高效使用补货报表
odoo·odoo19·库存报表·补货报表
odoo中国4 天前
如何在 Odoo 19 中创建序列
odoo·odoo19·自定义单据序列
odoo中国13 天前
Odoo 19 财务功能概述:财务模块中的定期存货计价(期末库存结转)
odoo·库存管理·财务管理·odoo19·库存计价·库存估值·期末库存结转
odoo中国15 天前
Odoo 19 库存功能实操:产品包装的设置与管理
odoo·仓库管理·odoo19·包装设置与管理
云草桑15 天前
Odoo 19.0 Docker Desktop快速部署 和Ubuntu24上安装1panel面板
运维·docker·容器·odoo
odoo中国20 天前
Odoo 19 采购功能:如何创建与管理产品类别,实现更智能的采购
odoo·odoo19·产品类别·采购类别·产品类别配置
Odoo老杨25 天前
成长型企业 ERP 系统选型:SAP 与 Odoo 免费开源 ERP 全面对比
sap·odoo·erp·中小企业数字化