在复杂的制造和供应链管理系统中,作为一名技术人员或架构师,我们经常会遇到两个核心概念:主生产计划(MPS)和物料需求计划(MRP)。虽然它们听起来有些相似,甚至在某些小型软件中被混为一谈,但在处理大规模生产逻辑时,它们有着本质的区别。
很多开发者在着手开发 ERP(企业资源计划)系统或库存管理模块时,往往容易混淆这两者的计算逻辑。如果你曾因为搞不清“独立需求”和“相关需求”而导致库存计算错误,或者好奇为什么系统有时运行 MPS 而有时运行 MRP,那么这篇文章正是为你准备的。
今天,我们将深入探讨 MPS 和 MRP 的本质区别,并通过实际的伪代码和算法逻辑,看看如何在代码层面实现这两种计划机制。让我们先从需求驱动因素说起,这是理解一切的基础。
核心驱动力:独立需求 vs 相关需求
在任何库存计划系统中,需求是唯一的驱动力。但在编写算法处理需求之前,我们必须深刻理解需求的两面性。这不仅仅是业务术语,更是我们设计数据库表结构和计算逻辑的基础。
什么是独立需求?
独立需求指的是那些直接来自客户订单或销售预测的物品需求。这意味着该物品的需求量不依赖于其他物品的需求。我们可以将其想象为树的“根”或“叶子”(取决于树的画法,通常是顶层)。
- 技术特征:这是成品(FGI, Finished Goods)或维修备件。它们具有独立的物料清单(BOM),或者根本没有 BOM(如果是购买后直接转售)。
什么是相关需求?
相关需求(也称为非独立需求)是指那些由于我们需要生产另一个“独立需求”物品而产生的需求。它们是组成成品的原材料或子组件。
- 技术特征:这些物品的需求量是通过计算 BOM 得出来的。例如,如果我们生产一辆自行车(独立需求),我们就必须有与之对应的两轮胎、一副车架(相关需求)。
简单来说:MPS 处理独立需求,MRP 处理相关需求。让我们深入了解它们各自的运作机制。
什么是主生产计划 (MPS)?
MPS 是生产计划系统的“大脑”。它主要负责确定我们要生产什么具体的成品,生产多少,以及什么时候交货。它专注于处理独立需求。
MPS 的工作原理与逻辑
MPS 并不是每天都会大幅变动的。通常我们按周或双周来运行 MPS。在运行时,系统会综合考虑:
- 实际销售订单:客户已经下单的要货请求。
- 销售预测:我们预计的客户会下的单。
- 库存可用量:仓库里现在有多少现货。
- 产能限制:我们工厂的机器和人手能不能干完这么多活。
MPS 的 Python 实现示例
让我们通过一段 Python 代码来看看如何构建一个简单的 MPS 计算器。这个脚本将演示如何根据订单和预测来计算主生产计划。
import pandas as pd
def calculate_mps(product_name, current_inventory, production_lot_size, orders_forecasts_list):
"""
计算主生产计划 (MPS)
参数:
product_name: 产品名称 (独立需求项)
current_inventory: 当前库存量
production_lot_size: 最小生产批量 (比如每次必须生产100个)
orders_forecasts_list: 列表,包含 {‘period‘: ‘周次‘, ‘order‘: 订单量, ‘forecast‘: 预测量}
返回:
打印出 MPS 报表
"""
print(f"
=== 正在为产品 [{product_name}] 生成 MPS ===")
# 模拟 ATP (可承诺量) 和 PAB (预计可用库存)
available_to_promise = current_inventory
projected_available_balance = current_inventory
print(f"{‘周次‘:<5} | {'订单':<6} | {'预测':<6} | {'MPS计划量':<10} | {'预计库存(PAB)':<12}")
print("-" * 65)
for week in orders_forecasts_list:
period = week['period']
order = week['order']
forecast = week['forecast']
# 逻辑:取订单和预测中的较大值作为需求(这是一种常见的策略,即备货生产)
gross_requirement = max(order, forecast)
# 判断是否需要生产:如果预计库存 < 0 或 < 安全库存(此处简化为0)
# 这里的逻辑稍微简化:需求无法用当前库存覆盖时,触发 MPS
mps_quantity = 0
if projected_available_balance < gross_requirement:
# 计算需要多少批次才能满足需求
batches_needed = (gross_requirement - projected_available_balance) // production_lot_size + 1
mps_quantity = batches_needed * production_lot_size
# 更新预计库存
projected_available_balance = projected_available_balance + mps_quantity - gross_requirement
# 输出结果
print(f"{period:<5} | {order:<6} | {forecast:<6} | {mps_quantity:<10} | {projected_available_balance:<12}")
# 实际应用场景:我们要生产 "高端机械键盘"
# 假设当前库存 50 个,最小生产批量为 100 个
mps_scenario = [
{'period': 'W1', 'order': 50, 'forecast': 20},
{'period': 'W2', 'order': 60, 'forecast': 30},
{'period': 'W3', 'order': 120, 'forecast': 50},
{'period': 'W4', 'order': 40, 'forecast': 40},
]
calculate_mps("高端机械键盘", current_inventory=50, production_lot_size=100, orders_forecasts_list=mps_scene)
代码解析:MPS 的关键决策点
在上面的代码中,你可以看到几个关键的 MPS 逻辑:
- 批量策略:我们设定了
production_lot_size。这非常符合实际生产情况——你不可能为了 5 个缺货的产品而开动流水线,通常会有一个最小批量(如 100、500)。 - 需求取大:
gross_requirement = max(order, forecast)。为了确保不缺货,我们通常取实际订单和预测值中较大的那个来计算。 - 库存平衡:MPS 的核心是维持
Projected Available Balance为正数,这就是 MPS 的主要职责——不让产线停工,也不让客户缺货。
什么是物料需求计划 (MRP)?
一旦 MPS 生成,确定了我们要生产多少个“键盘”,下一步就是搞清楚我们需要多少“轴体”、“键帽”和“PCB板”。这就是 MRP 的职责。
MRP 针对的是相关需求。它是一个逐层展开的过程,依赖于 BOM(物料清单)结构。
MRP 的计算逻辑与展开
MRP 的核心算法通常被称为“逐层展开法”。它不仅考虑父项的需求,还要考虑子项现有的库存、在途库存和安全库存。
- 毛需求:由 MPS 的产量 × BOM 清单数量得出。
- 净需求:毛需求 – 现有库存 – 在途库存。
- 提前期:MRP 必须比 MPS 提前运行,因为原材料采购需要时间。
MRP 的 JavaScript 实现示例
接下来,让我们看看如何在前端或后端使用 JavaScript 来计算一个简单的 MRP 场景。假设我们已知 MPS 计划生产 100 个键盘,现在我们要计算需要采购多少“Cherry 轴体”。
// 定义 BOM 结构:1个键盘需要 90 个轴体
const BOM = {
"KEYBOARD": {
components: [
{ itemId: "SWITCH_RED", quantityPerParent: 90 }, // 90个轴体
{ itemId: "KEYCAP_PBT", quantityPerParent: 104 } // 104个键帽
]
}
};
// 模拟当前库存状态 (通常来自数据库)
const inventoryStatus = {
"SWITCH_RED": { onHand: 500, leadTime: 7 }, // 库存500,提前期7天
"KEYCAP_PBT": { onHand: 200, leadTime: 14 } // 库存200,提前期14天
};
/**
* 运行 MRP 计算
* @param {string} parentId - MPS 产生的父项 ID
* @param {number} parentQuantityToProduce - MPS 计划生产的数量
*/
function runMRP(parentId, parentQuantityToProduce) {
console.log(`
--- 正在运行 MRP 计算: 计划生产 ${parentQuantityToProduce} 个 ${parentId} ---`);
const bomData = BOM[parentId].components;
if (!bomData) {
console.log("未找到 BOM 清单,无法运行 MRP");
return;
}
bomData.forEach(component => {
const itemId = component.itemId;
const qtyPerParent = component.quantityPerParent;
// 1. 计算毛需求
const grossRequirement = parentQuantityToProduce * qtyPerParent;
// 2. 获取当前库存
const currentStock = inventoryStatus[itemId].onHand;
// 3. 计算净需求 (简单算法:不考虑在途)
let netRequirement = grossRequirement - currentStock;
let plannedOrderReceipt = 0;
if (netRequirement > 0) {
plannedOrderReceipt = netRequirement;
console.log(`[需采购] 组件: ${itemId}`);
console.log(` - 毛需求: ${grossRequirement}`);
console.log(` - 现有库存: ${currentStock}`);
console.log(` - 净需求: ${netRequirement}`);
console.log(` - 建议补货数量: ${netRequirement}`);
console.log(` - 需在第 ${inventoryStatus[itemId].leadTime} 天前下单 (提前期)`);
} else {
console.log(`[库存充足] 组件: ${itemId}. 无需额外采购`);
}
console.log("------------------------------------------------");
});
}
// 场景:MPS 计划生产 100 个键盘,MRP 计算原材料
runMRP("KEYBOARD", 100);
常见错误与陷阱:MRP 计算
在实现 MRP 逻辑时,新手开发者容易犯以下错误:
- 忽略提前期偏移:如果你今天计算出来缺 100 个螺丝,且螺丝采购要 7 天,那么订单释放日期应该是今天减去 7 天,而不是今天。代码中必须处理好时间偏移(Time Phasing)。
- 低阶码的问题:如果原材料同时是多个产品的组件,MRP 必须汇总所有父项的需求后再统一计算,不能分开算。这需要算法在最后进行“层级合并”。
MRP 与 MPS 的实战对比
为了让我们在系统架构中更清晰地定位这两个模块,让我们通过一个经典的例子来区分它们:圆珠笔的生产。
场景设定
- MPS 物品(顶层):圆珠笔成品。
原因*:客户只买笔,不买笔芯组件。笔的需求来自客户的订单,这是独立需求。
- MRP 物品(底层):笔帽、笔杆、笔芯、弹簧。
原因*:我们只在生产笔时才需要笔芯。如果没人买笔,我们就不会生产笔芯。笔芯的需求完全依赖于笔的数量,这是相关需求。
运行频率与系统负载
MPS 并不是每天都要跑的。通常,主生产计划员会在每周一根据最新的销售数据调整 MPS。一旦 MPS 确认了,MRP 系统可能会每天甚至每小时运行,因为物料的库存变动(如废品损耗、紧急领料)是非常频繁的。
- 优化建议:如果你的系统性能吃紧,考虑将 MPS 运行与 MRP 运行分离开来。MPS 可以作为定时任务,而 MRP 可以通过库存变动事件来触发(虽然这可能导致计算碎片化,但响应更快)。
总结与最佳实践
我们在上文详细拆解了 MPS 和 MRP 的区别,并提供了一些基础的代码实现。作为技术人员,在设计这类系统时,请记住以下几点:
- 边界清晰:MPS 只负责搞定“卖什么”,MRP 负责搞定“用什么造”。不要在 MRP 的计算逻辑里加入销售预测的干扰,MPS 应该是 MRP 唯一的输入来源(在主计划模式下)。
- 数据的一致性:BOM 的准确性直接决定了 MRP 的可用性。如果 BOM 说 1 个键盘需要 200 个轴体,你的代码再完美,算出来的结果也是垃圾(GIGO原则)。
- 性能优化:BOM 的爆炸运算(Exploding)是指数级的。在处理多层级 BOM(如汽车有数万个零件)时,要注意算法的时间复杂度,避免递归导致的栈溢出,或循环依赖导致的死循环。
希望这篇文章能帮助你理清 MRP 和 MPS 的迷雾。在下一步的开发中,你可以尝试扩展示例代码,加入“安全库存”和“废品率”参数,让你的库存计划系统更加健壮。