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 机制!如有疑问,欢迎讨论。

相关推荐
云草桑12 天前
15分钟快速了解 Odoo
数据库·python·docker·postgresql·.net·odoo
山上春15 天前
Odoo 18 Web 客户端架构深度解析与 Navbar 差异化定制研究报告
odoo
山上春22 天前
ONLYOFFICE Odoo 集成架构深度解析与实战手册(odoo文件预览方案)
架构·odoo
odoo中国1 个月前
如何在 Odoo 19 中创建日历视图
odoo·odoo19·odoo 视图开发·日历视图配置·alendar 标签使用·odoo 日程管理
odoo中国1 个月前
如何在 Odoo 19 中加载演示数据
xml·csv·odoo·odoo 19·odoo 演示数据加载
odoo中国1 个月前
Odoo 19 模块结构概述
开发语言·python·module·odoo·核心组件·py文件按
odoo中国1 个月前
如何在 Odoo 中从 XML 文件调用函数
xml·odoo·odoo开发·调用函数
odoo中国1 个月前
Odoo 19 中的基础视图有哪些?
odoo·odoo19·基础视图
李怀瑾2 个月前
在Odoo18中实现多选下拉框搜索功能
odoo