本文将深入剖析 Odoo 库存管理中的 MTO(按订单生产)和智能MTO(MTS else MTO)机制,结合源码分析和实际案例,帮助你完全掌握这套核心的采购补货策略。
目录
- 核心概念
- 数据库模型设计
- [MTO 工作流程详解](#MTO 工作流程详解)
- [智能MTO 核心算法](#智能MTO 核心算法)
- 规则优先级机制
- [采购 vs 生产决策](#采购 vs 生产决策)
- 实战案例
- 配置指南
- 调试技巧
一、核心概念
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 使用两个字段控制规则优先级:
- route_sequence (路线序号) - 第一优先级
- 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: 通过产品表单
- 进入 产品 → 产品 → 选择产品
- 切换到 库存 选项卡
- 在 路线 部分勾选:
- ☑ 补货按订单 (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
- 进入 库存 → 配置 → 路线
- 选择 Replenish on Order (MTO) 路线
- 切换到 规则 选项卡
- 编辑规则,修改 Supply Method :
Take From Stock= 普通MTSTrigger Another Rule= 纯MTOTake 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: MTO不触发采购/生产
原因分析:
python
# 检查产品配置
product.route_ids # 是否包含MTO路线?
product.seller_ids # 采购需要供应商
product.bom_ids # 生产需要BOM
解决方案:
- 确保产品勾选了MTO路线
- 采购模式需配置供应商
- 生产模式需创建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.sequence 和 rule.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) # 排序字段存储
十二、总结
核心要点回顾
-
MTO vs 智能MTO
- MTO: 总是触发采购/生产
- 智能MTO: 优先使用库存,不足时才触发
-
规则优先级
- 两级排序:
route_sequence→sequence - 数值越小越优先
- 两级排序:
-
采购/生产决策
- 通过
rule.action字段决定 buy= 采购,manufacture= 生产
- 通过
-
智能MTO算法
- 采购量 = max(需求量 - 可用库存, 0)
- 批量查询库存优化性能
适用场景总结
| 策略 | 适用产品 | 优势 | 劣势 |
|---|---|---|---|
| 纯MTS | 快消品 | 响应快 | 库存成本高 |
| 纯MTO | 定制品 | 零库存 | 交期长 |
| 智能MTO | 常规品 | 平衡库存和缺货 | 配置复杂 |
最佳实践
-
分层配置
- A类产品: 智能MTO
- B类产品: 纯MTS
- C类产品: 纯MTO
-
定期审查
- 每季度检查规则有效性
- 根据周转率调整策略
-
监控指标
- 库存周转率
- 缺货率
- 采购订单数量
- 平均交付时间
相关资源
官方文档
源码文件
addons/stock/models/stock_rule.pyaddons/stock/models/stock_move.pyaddons/purchase_stock/models/stock_rule.pyaddons/mrp/models/stock_rule.py
希望这篇文章能帮助你深入理解 Odoo 的 MTO 和智能MTO 机制!如有疑问,欢迎讨论。