离散制造(Discrete Manufacturing)里常见的生产模式有MTS(Make To Stock)和MTO(Make To Order)。MTO强调按订单生产、产品往往有较高的定制化和多变的BOM。对于制造企业来说,一套贴合 MTO 特性的 ERP 系统,能把“订单 → 设计 → 物料 → 计划 → 生产 → 交付”这条链路串通起来,降低交付风险、减少呆滞物料、提高响应速度和客户满意度。本文将把如何搭建一套面向离散制造—MTO 的 ERP 系统拆成可执行的步骤,给出架构图、流程图,以及一个完整的参考代码片段,帮助企业和开发团队落地实施。
本文你将了解
注:本文示例所用方案模板:简道云ERP系统,给大家示例的是一些通用的功能和模块,都是支持自定义修改的,你可以根据自己的需求修改里面的功能。
简单说:ERP 是企业资源计划系统,但不同企业、不同生产模式下 ERP 的重点不同。离散制造 MTO 的特点是:
因此,MTO ERP 要解决的主要问题是:如何在保证交期的情况下,动态展开 BOM、计算物料需求、触发采购与生产排程、并把成本和财务信息准确落地。
下面用 Mermaid 给出一个高层架构图——这是一个典型的微服务/模块化设计,适配中大型制造企业的扩展需求。
graph LR
subgraph Users
A[销售/客服] -->|web| Frontend
B[计划/车间] -->|web/移动| Frontend
C[采购/仓库] -->|web| Frontend
D[财务/管理] -->|web| Frontend
end
Frontend --> API_GW[API 网关]
API_GW --> Auth[认证服务]
API_GW --> MSales[销售服务]
API_GW --> MCustomer[客户服务]
API_GW --> MProd[生产服务]
API_GW --> MPur[采购服务]
API_GW --> MInv[库存服务]
API_GW --> MFin[财务服务]
API_GW --> MTech[技术管理/PLM接口]
MProd -->|events| MQ[消息队列(RabbitMQ/Kafka)]
MPur -->|events| MQ
MInv -->|events| MQ
subgraph Persistence
DBMain[(Postgres)]
Cache[(Redis)]
FileStore[(对象存储)]
end
MSales --> DBMain
MProd --> DBMain
MInv --> DBMain
MFin --> DBMain
Auth --> DBMain
MQ --> DBMain
MTech --> FileStore
subgraph Integration
PLM[PLM/CAD]
MES[MES/车间系统]
ERP3P[第三方ERP/供应商门户]
end
MTech --> PLM
MProd --> MES
MPur --> ERP3P
核心组件说明:
下面把每个模块的关键职责和实现建议写清楚,并给出典型字段/接口建议。
职责:管理产品结构(BOM)、工艺(Routings)、物料主数据、版本与工程变更(ECO/ECN)。 要点:
职责:客户档案、联系人、账期、信用额度、送货地址与交货条款。 要点:
职责:订单录入、报价、合同、订单确认、交期评估(ATP/可承诺发货能力)。 要点:
职责:物料需求计算(MRP)、主生产计划(MPS)、排产(APS)、车间执行(MES 对接)、完工入库。 要点:
职责:供应商管理、采购申请、采购订单、到货检验、供应能力协同(供需看板)。 要点:
职责:库存账、批次/序列号管理、在途、占用与可用量(ATP)、仓储作业(入库/出库/调拨)。 要点:
职责:应收/应付、成本核算(标准成本/实时报表)、结算、发票管理。 要点:
flowchart TB
A[客户下单] --> B[销售录入/配置]
B --> C{是否标准BOM?}
C -->|是| D[生成生产订单]
C -->|否| E[派工工程/生成新BOM版本]
D --> F[MRP:计算物料需求]
E --> F
F --> G{物料可用?}
G -->|是| H[排产并下发派工]
G -->|否| I[触发采购/外协]
I --> J[供应到货后入库]
J --> H
H --> K[车间执行(MES)]
K --> L[完工入库]
L --> M[质检出库/发运]
M --> N[账务结算/成本核算]
BOM 展开就是从成品沿树向下,把每个组件按数量系数乘开,得到物料需求清单(MRP 的输入)。
flowchart LR
A[主产品] --> B[零件A x2]
A --> C[零件B x1]
B --> D[原料A1 x3]
C --> E[原料B1 x2]
说明:BOM 要支持替代件、备选供应商与采购批量规则(MOQ、批量折扣)。
下面给出一个 Node.js(TypeScript)示例:包含数据库 schema、关键 API(订单创建)、BOM 展开函数、简单 MRP/排产触发示例。示例基于 Express + TypeORM + RabbitMQ。代码为参考级别,真实产品需要补充权限、安全与完整错误处理。
// package.json (仅依赖展示)
// {
// "name": "mto-erp-sample",
// "dependencies": {
// "express": "^4.18.2",
// "typeorm": "^0.3.12",
// "pg": "^8.10.0",
// "amqplib": "^0.8.0",
// "reflect-metadata": "^0.1.13"
// }
// }
/* src/entity/Material.ts */
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class Material {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
code: string;
@Column()
name: string;
@Column({ default: 0 })
leadTimeDays: number; // 采购交期
@Column({ default: 0 })
safetyStock: number;
}
/* src/entity/BOM.ts */
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm";
@Entity()
export class BOM {
@PrimaryGeneratedColumn()
id: number;
@Column()
productCode: string; // 成品/半成品代码
@Column()
componentCode: string;
@Column("float")
qty: number; // 每个成品需要的数量
@Column({ nullable: true })
effectiveFrom?: Date;
@Column({ nullable: true })
version?: string;
}
/* src/entity/SalesOrder.ts */
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
@Entity()
export class SalesOrder {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
orderNo: string;
@Column()
productCode: string;
@Column("int")
qty: number;
@Column()
dueDate: Date;
@Column({ default: 'CREATED' })
status: string;
}
/* src/app.ts */
import express from "express";
import "reflect-metadata";
import { DataSource } from "typeorm";
import { Material } from "./entity/Material";
import { BOM } from "./entity/BOM";
import { SalesOrder } from "./entity/SalesOrder";
import amqp from "amqplib";
const app = express();
app.use(express.json());
const AppDataSource = new DataSource({
type: "postgres",
host: process.env.DB_HOST || "localhost",
port: 5432,
username: "postgres",
password: "postgres",
database: "mto_erp",
entities: [Material, BOM, SalesOrder],
synchronize: true,
});
let channel: amqp.Channel | null = null;
async function initMQ(){
const conn = await amqp.connect(process.env.RABBIT_URL || "amqp://localhost");
channel = await conn.createChannel();
await channel.assertQueue("order.events", { durable: true });
}
AppDataSource.initialize().then(() => {
console.log("DB connected");
initMQ().then(()=>console.log("MQ connected"));
});
/* Helper: 展开 BOM(多级) */
async function explodeBOM(productCode: string, qty: number, ds: DataSource) {
const bomRepo = ds.getRepository(BOM);
const materialNeeds: Record<string, number> = {};
// DFS 展开
async function dfs(code: string, multiplier: number) {
const nodes = await bomRepo.find({ where: { productCode: code } });
if (!nodes || nodes.length === 0) {
// code 是原料或外购件
materialNeeds[code] = (materialNeeds[code] || 0) + multiplier;
return;
}
for (const n of nodes) {
await dfs(n.componentCode, multiplier * n.qty);
}
}
await dfs(productCode, qty);
return materialNeeds; // { materialCode: totalQty }
}
/* 简单 MRP:根据库存/安全库存判断是否需要采购(示例短版) */
async function simpleMRP(productCode: string, qty: number, ds: DataSource) {
const needs = await explodeBOM(productCode, qty, ds);
const materialRepo = ds.getRepository(Material);
const results: Array<{ code: string; need: number; onHand: number; toPurchase: number }> = [];
for (const code of Object.keys(needs)) {
const m = await materialRepo.findOneBy({ code });
const onHand = m ? 0 : 0; // 假设从库存服务获取,示例里用 0
const safety = m ? m.safetyStock : 0;
const need = needs[code];
const toPurchase = Math.max(0, need + safety - onHand);
results.push({ code, need, onHand, toPurchase });
}
return results;
}
/* 创建订单 API:保存订单 -> 触发简单 MRP -> 发布事件给采购/生产 */
app.post("/orders", async (req, res) => {
const ds = AppDataSource;
const repo = ds.getRepository(SalesOrder);
const { orderNo, productCode, qty, dueDate } = req.body;
try {
const so = repo.create({ orderNo, productCode, qty, dueDate, status: 'CONFIRMED' });
await repo.save(so);
// 1) 计算物料需求
const mrp = await simpleMRP(productCode, qty, ds);
// 2) 发布事件
const payload = { type: 'OrderConfirmed', data: { orderNo, productCode, qty, mrp } };
if (channel) {
channel.sendToQueue("order.events", Buffer.from(JSON.stringify(payload)), { persistent: true });
}
return res.json({ ok: true, order: so, mrp });
} catch (err) {
console.error(err);
return res.status(500).json({ ok: false, message: 'error' });
}
});
app.listen(3000, () => {
console.log("Server started on 3000");
});
实现后企业常见收益指标:
MTO(按单生产)与 MTS(按库存生产)最大的不同在“需求驱动的起点”和“库存应对方式”。MTS 更强调安全库存与预测驱动的补货,ERP 需侧重预测与补货策略;而 MTO 则是订单驱动,ERP 的核心是快速把单转换成可执行的生产计划并精确计算物料需求。具体到系统设计,MTO 系统要更强调 BOM 的配置能力、订单级成本核算、工程变更管理、以及灵活的排产策略(以响应订单交期)。此外,MTO 更依赖与供应链的即时协同——供应商交期、外协能力、在途可视化都更关键。
对于中小企业(几十人规模),直接从微服务开始并不总是最优选择。微服务带来的分布式复杂性(运维、监控、分布式事务、部署)会消耗大量资源。建议先走模块化单体或按边界上下文拆分的单体应用,重用团队开发/测试部署流程;当系统增长到一定规模或需要按模块独立扩展(例如生产服务需要高并发计算)时,再逐步把热点模块拆成微服务。无论选哪种路径,都要从一开始做好清晰的领域建模、API 设计和数据隔离,这样将来拆分会容易很多。
BOM 和工艺路线应当支持版本控制与生效控制。实现上需把每个 BOM 记录为包含版本号与生效日期的实体;工程变更(ECO/ECN)应是一个工作流:申请 → 审核 → 排期生效。系统应支持“历史订单使用当时生效的 BOM/工艺”,也就是说在订单确认阶段就要把所用的 BOM 版本快照到订单/生产工单里,以便追溯。对于设计频繁变更的企业,推荐与 PLM 系统深度集成,并在 ERP 里保留 BOM 快照和变更日志,质检与财务也能依据不同版本进行成本与质量分析。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。