首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >如何开发ERP(离散制造-MTO)系统中的销售管理板块(附架构图+流程图+代码参考)

如何开发ERP(离散制造-MTO)系统中的销售管理板块(附架构图+流程图+代码参考)

原创
作者头像
用户5667915
发布2025-09-17 16:09:18
发布2025-09-17 16:09:18
1340
举报

很多公司做 ERP 时把注意力放在 BOM、生产排产、财务凭证上,结果把“销售”当成一个简单的单据模块:开单 —> 发货。

对于离散制造 + MTO(按订单生产) 企业来说,销售管理是整个业务的入口和“决策中心”:

  • 报价决定毛利与成交概率;
  • 订单驱动生产和采购;
  • 交期承诺直接影响客户满意度与供应链节奏。

因此,做得好能减少沟通、降低交付延期、提高现金回收;

做不好,则频繁改单、加急、库存错配、利润受损。


本文你将了解

  1. 什么是 ERP与销售板块的核心职责
  2. 总体架构(含架构图)
  3. 关键业务流程(含流程图)
  4. 功能拆解
  5. 数据模型设计与索引建议
  6. 接口与事件设计(同步/异步边界)
  7. 开发技巧与工程实践(ATP、并发、幂等、审计、测试、性能)
  8. 实现示例
  9. 验收标准与落地建议

注:本文示例所用方案模板:简道云ERP系统,给大家示例的是一些通用的功能和模块,都是支持自定义修改的,你可以根据自己的需求修改里面的功能。


1. 什么是 ERP(离散制造 - MTO)系统,销售板块为什么重要

离散制造:制造对象是可区分、可拆解的件(设备、机床、零部件)。MTO(Make To Order)意味着大多数生产在接到订单后才启动,库存低、交期关键。销售模块既要做前端的报价和订单管理,还得和物料、BOM、工艺、供应商交期紧耦合:当销售承诺一个交期,需要立刻知道关键件能否按时到位并反馈风险;报价需要结合成本和可变费用来估算毛利,从而指导商务策略。


2.高层架构

代码语言:txt
复制
+------------------------------------------------------------+
| 客户/销售前端 (Web/移动/自助门户)                          |
+-----------------------------+------------------------------+
                              |
                API 网关(鉴权、限流、统一日志、API 版本) 
                              |
+-----------+--------+-------+---------+---------------------+
| Sales Svc | Pricing| Inv Svc| Scheduler | Integration Svc |
| (quotes,  | Svc    | (stock,| (MRP/APS) | (MES/财务/CRM)  |
| orders)   | rules  | ATP)   |           |                 |
+-----------+--------+-------+---------+---------------------+
       |               |         |           |
       +---------------+---------+-----------+
                       |
                 事件总线(Kafka / RabbitMQ / Redis Stream)
                       |
                支撑服务:采购、仓库、排产、质检、财务

要点:

  • 拆分服务降低耦合:报价/订单/库存/定价可以分别伸缩。
  • 事件总线用于跨服务异步协同(避免分布式事务)。
  • API 网关统一鉴权(OAuth2/JWT)、审计、限流。

3. 关键业务流程

报价 -> 转订单 -> ATP -> 备料排产 -> 出库 -> 售后(退换货)

  1. 销售/客户在系统提交配置化报价请求(含选配项与特殊工艺)。
  2. 报价服务调用定价引擎(考虑物料成本、工时、外协费用、运输安装等),生成报价单并进入审批流(可选)。
  3. 报价确认后,生成销售订单;系统触发 order.created 事件。
  4. 库存服务对订单行进行 ATP(软检:当前可用量;硬检:在途采购+排产),返回可交期或风险。
  5. 若关键件缺料,触发采购建议/加急单;如果通过,则进入排产并预约产能。
  6. 生产/仓库完成入库与检验后,触发出库拣货、装箱、发运并更新订单状态。
  7. 客户签收后生成应收与财务凭证,如发生退货/换货则走售后流程并联动质检与财务。

4. 功能拆解

4.1 销售管理看板

  • 展示:新建/待确认订单、逾期订单、关键物料缺料列表、出货计划、毛利趋势。
  • 交互:支持按客户/工厂/业务员/产品线筛选,点击订单可穿透到工单、库存和采购建议。
  • 技术点:看板数据大多为汇总,建议用预聚合表 + Redis 缓存 + 定时/事件触发刷新。

4.2 报价单

  • 支持配置化产品(选型逻辑)、费用项(运输/检测/安装)、折扣与审批流、版本管理。
  • 需要支持“成本仿真”:基于当前采购价、工时、外协报价估算毛利。
  • 审批: 可建规则(金额/客户等级/折扣率),超权限走逐级审批并记录审批意见。

4.3 销售订单

  • 订单转化逻辑:报价确认或手工下单生成订单;支持合单/拆单、分批交付。
  • 订单状态机:NEW -> CONFIRMED -> ALLOCATED -> PICKED -> SHIPPED -> CLOSED(并记录变更日志)。
  • 订单变更:需支持价格/数量/交期变更,变更要触发重新 ATP 和排产调整,并记录历史版本。

4.4 销售出库

  • 支持按订单拣货、按交货单分装箱、序列号/批次管理、出库检验。
  • 出库应生成装箱单、物流单号并通知客服/客户。
  • 出库与财务联动:发运触发应收账款或收入确认(按企业会计政策)。

4.5 销售退货

  • 流程:客户申请 -> 创建退货单 -> 仓库/质检检验 -> 判定(入库/返修/报废) -> 退款/抵扣/换货。
  • 需要记录证据(图片/报告)与检验结论,退货影响库存并触发财务记账。

4.6 销售换货

  • 换货可以是“先退后发”或“先发后退”,系统需支持两种策略并记录费用分摊(运费、维修费)。
  • 若涉及质保期,系统应自动回溯原销售订单,判断是否免费换货或按比例收费。

4.7 报表与统计

  • 报价统计:报价量、通过率(成交率)、平均折扣、平均报价响应时间、报价毛利估算。
  • 订单统计:准时交付率、履约率、订单周期(下单到发运)、退货率、主因分析(物料 / 产能 / 质检)。
  • 技术:定期 ETL 到数据仓库(或使用 OLAP),给 BI 看板(PowerBI / Superset)。

5. 数据模型

示例字段:

  • customers:id, code, name, contact(json), credit_limit, payment_terms
  • products:id, sku, name, is_serial(boolean), default_uom, lead_time_days, bom_id
  • quotes:id, quote_no, customer_id, status, created_by, valid_until, total_amount, currency
  • quote_lines:id, quote_id, product_id, qty, unit_price, line_amount, options(json)
  • sales_orders:id, order_no, quote_id, customer_id, status, order_date, promised_date, total_amount
  • order_lines:id, order_id, product_id, qty, unit_price, delivery_date, status
  • stock:product_id, location_id, qty_on_hand, qty_reserved, batch, serial
  • stock_moves:id, type(order/pick/return), ref_id, product_id, qty, from_loc, to_loc, status
  • shipments:id, shipment_no, order_id, ship_date, carrier, tracking_no, status
  • returns:id, return_no, order_id, reason, inspected_result, action

索引建议:

  • 唯一索引:sales_orders(order_no)、quotes(quote_no)
  • 查询索引:order_lines(order_id, product_id)、stock(product_id, location_id)
  • 高频排序/分页字段上建复合索引(如 sales_orders(customer_id, order_date))

6. 接口与事件设计

6.1 关键 REST API

  • POST /api/quotes:创建报价
  • GET /api/quotes/:id:查询报价
  • POST /api/quotes/:id/confirm:确认报价(生成订单)
  • POST /api/orders:创建订单(或由报价转化)
  • GET /api/orders/:id:订单详情
  • POST /api/orders/:id/lock-stock:尝试库存锁定(幂等)
  • POST /api/shipments:创建出库单并触发发运
  • POST /api/returns:创建退货单

6.2 事件

  • order.created -> payload: { orderId }
  • order.updated
  • stock.locked / stock.lock_failed
  • shipment.dispatched
  • return.created / return.inspected

原则:用户交互(报价创建、订单创建)为同步体验;库存锁定/硬 ATP/排产可以是异步,前端通过看板与通知追踪结果。


7. 开发技巧与工程实践

ATP 设计

  • 软检(快速):基于当前可用库存 - 已保留量,返回“可用/不可用/部分可用”。用于快速响应客户。
  • 硬检(深入):计算在途采购、生产排期、外协交期。硬检耗时,放异步任务并在看板展示“可交期”与“风险点”。

幂等与并发

  • 每次外部请求(例如库存锁定)要用 idempotency_key 保证幂等。
  • 库存变更采用乐观锁(version)或数据库行级锁 (SELECT ... FOR UPDATE)。

事件驱动

  • 事件需幂等(事件处理记录消费位移或消费 ID),并支持重试与死信队列。
  • 用事件替代分布式事务:订单写入后发布事件,订阅方做自己的事务与回滚补偿。

审计与可追溯

  • 状态变更、审批记录、价格变更、退货检验都要留痕(谁、何时、原因、附件)。

性能

  • 报价模拟和成本计算可走异步批量,或预缓存关键成本数据(最近采购价、外协成本)。
  • 看板与统计使用物化视图或定时聚合表,避免实时复杂 JOIN。

测试

  • 单元测试(业务逻辑)、集成测试(数据库)、契约测试(服务间 API)和端到端测试(订单流)都要覆盖。

8. 代码实现

代码语言:txt
复制
后端:server.js(简化)
// server.js
const express = require('express');
const bodyParser = require('body-parser');
const { Pool } = require('pg');
const EventEmitter = require('events');
const bus = new EventEmitter();
const pool = new Pool({
  host: process.env.PG_HOST || 'localhost',
  user: process.env.PG_USER || 'postgres',
  password: process.env.PG_PASS || 'password',
  database: process.env.PG_DB || 'erp',
  port: process.env.PG_PORT || 5432,
});
const app = express();
app.use(bodyParser.json());
// 创建报价
app.post('/api/quotes', async (req, res) => {
  const { customer_id, items, valid_until } = req.body;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const r = await client.query(
      `INSERT INTO quotes (customer_id, status, total_amount, valid_until) VALUES ($1,$2,$3,$4) RETURNING id`,
      [customer_id, 'DRAFT', 0, valid_until]
    );
    const quoteId = r.rows[0].id;
    // items 是 [{product_id, qty, unit_price}]
    for (const it of items) {
      await client.query(
        `INSERT INTO quote_lines (quote_id, product_id, qty, unit_price, line_amount) VALUES ($1,$2,$3,$4,$5)`,
        [quoteId, it.product_id, it.qty, it.unit_price, it.qty * it.unit_price]
      );
    }
    // 简单计算 total
    const t = await client.query(`SELECT SUM(line_amount) as total FROM quote_lines WHERE quote_id=$1`, [quoteId]);
    await client.query(`UPDATE quotes SET total_amount=$1 WHERE id=$2`, [t.rows[0].total || 0, quoteId]);
    await client.query('COMMIT');
    res.status(201).json({ id: quoteId });
  } catch (err) {
    await client.query('ROLLBACK');
    console.error(err);
    res.status(500).send('error');
  } finally {
    client.release();
  }
});
// 报价确认 -> 生成订单并发布事件
app.post('/api/quotes/:id/confirm', async (req, res) => {
  const quoteId = req.params.id;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const q = await client.query(`SELECT * FROM quotes WHERE id=$1 FOR UPDATE`, [quoteId]);
    if (q.rowCount === 0) return res.status(404).send('quote not found');
    const quote = q.rows[0];
    if (quote.status !== 'DRAFT') return res.status(400).send('invalid status');
    const ord = await client.query(
      `INSERT INTO sales_orders (quote_id, customer_id, status, order_date, total_amount) VALUES ($1,$2,$3,$4,$5) RETURNING id`,
      [quoteId, quote.customer_id, 'NEW', new Date(), quote.total_amount]
    );
    const orderId = ord.rows[0].id;
    // copy lines
    const lines = await client.query(`SELECT product_id, qty, unit_price FROM quote_lines WHERE quote_id=$1`, [quoteId]);
    for (const ln of lines.rows) {
      await client.query(
        `INSERT INTO order_lines (order_id, product_id, qty, unit_price, status) VALUES ($1,$2,$3,$4,$5)`,
        [orderId, ln.product_id, ln.qty, ln.unit_price, 'NEW']
      );
    }
    await client.query(`UPDATE quotes SET status='CONFIRMED' WHERE id=$1`, [quoteId]);
    await client.query('COMMIT');
    // 发布事件(事务外)
    bus.emit('order.created', { orderId });
    res.json({ ok: true, orderId });
  } catch (err) {
    await client.query('ROLLBACK');
    console.error(err);
    res.status(500).send('error');
  } finally {
    client.release();
  }
});
// 监听 order.created 并尝试锁库存(示例)
bus.on('order.created', async (payload) => {
  console.log('event order.created', payload);
  // 这里实际应调用库存服务 API 或发送消息到队列
  // 简单示例:读取订单行并标记 reserve(伪操作)
  const client = await pool.connect();
  try {
    const lines = await client.query(`SELECT product_id, qty FROM order_lines WHERE order_id=$1`, [payload.orderId]);
    for (const ln of lines.rows) {
      // 伪逻辑:插入 stock_moves 表当作锁定请求
      await client.query(
        `INSERT INTO stock_moves (type, ref_id, product_id, qty, from_loc, to_loc, status) VALUES ($1,$2,$3,$4,$5,$6,$7)`,
        ['reserve', payload.orderId, ln.product_id, ln.qty, 'WH:MAIN', 'RESERVE', 'PENDING']
      );
    }
    // 这里可触发异步任务处理实际库存扣减
  } catch (err) {
    console.error(err);
  } finally {
    client.release();
  }
});
app.listen(3000, () => console.log('listening 3000'));

数据库示例(Postgres)

代码语言:txt
复制
CREATE TABLE customers (id serial PRIMARY KEY, code text UNIQUE, name text);
CREATE TABLE products (id serial PRIMARY KEY, sku text UNIQUE, name text, lead_time_days int);
CREATE TABLE quotes (id serial PRIMARY KEY, quote_no text, customer_id int references customers(id), status text, total_amount numeric, valid_until date);
CREATE TABLE quote_lines (id serial PRIMARY KEY, quote_id int references quotes(id), product_id int references products(id), qty numeric, unit_price numeric, line_amount numeric);
CREATE TABLE sales_orders (id serial PRIMARY KEY, order_no text, quote_id int references quotes(id), customer_id int references customers(id), status text, order_date timestamp, total_amount numeric);
CREATE TABLE order_lines (id serial PRIMARY KEY, order_id int references sales_orders(id), product_id int references products(id), qty numeric, unit_price numeric, status text);
CREATE TABLE stock_moves (id serial PRIMARY KEY, type text, ref_id int, product_id int references products(id), qty numeric, from_loc text, to_loc text, status text);

前端 React 示例(创建报价并确认)

代码语言:txt
复制
// QuoteForm.jsx
import React, { useState } from 'react';
import axios from 'axios';
export default function QuoteForm(){
  const [items, setItems] = useState([{ product_id:1, qty:1, unit_price:100 }]);
  async function createQuote(){
    const res = await axios.post('/api/quotes', { customer_id:1, items, valid_until: '2025-12-31' });
    alert('报价创建 ID: ' + res.data.id);
  }
  async function confirmQuote(id){
    const res = await axios.post(`/api/quotes/${id}/confirm`);
    alert('已确认,订单ID: ' + res.data.orderId);
  }
  return (
    <div>
      <h3>报价(示例)</h3>
      <button onClick={createQuote}>创建报价</button>
      <button onClick={() => confirmQuote(1)}>确认报价ID=1(示例)</button>
    </div>
  );
}


9. 验收标准与交付建议

建议分阶段交付:

  • 阶段 1(基础流):报价创建 → 报价确认 → 生成订单 → 基本出库流程。验收:10 条样例订单完整走通,出库单生成,库存变更正确。
  • 阶段 2(ATP & 统计):实现软 ATP、硬 ATP(异步)、报价毛利模拟、报价审批流。验收:ATP 判定与看板风险提示正确;报价毛利与手工核算误差 <5%。
  • 阶段 3(自动化 & 优化):事件驱动联动采购/排产、看板自定义、性能 & 高可用。验收:系统在 X 并发下响应时间 <200ms(查询类)和能处理并发报价模拟 N 并发(按业务需求定义)。

关键交付件:API 文档(OpenAPI)、ER 图、事件契约文档、运维/备份方案、单元/集成测试报告。


10. 开发中常见问题与解决思路

  • 报价多配置复杂:把配置选项模块化,做配置模板与规则引擎(避免把逻辑硬编码)。
  • 库存冲突:库存锁定用幂等、乐观锁、并行控制,必要时按 SKU 分片处理。
  • 退货占用库存:退货入库要先经过质检再真正增加可用量,并在入库时记录来源与批次。
  • 复杂审批影响效率:审批规则建议配置化(基于金额/客户/折扣),支持并行审批和超时提醒。

11. 结论

  1. 对 MTO 企业,销售管理模块是“把订单可靠地变成生产指令”的关键入口。投资在报价模拟、ATP 与事件驱动的自动化上,能显著提升履约率。
  2. 建议先做小步快跑的 MVP(报价 + 订单 + 出库),再逐步加 ATP、自动下采购、看板与统计。
  3. 强调审计与追溯:销售变更、退货检验、报价审批都要有可查证据,便于责任归属和改进闭环。

FAQ

FAQ 1:在 MTO 场景下,如何设计可用性检查(ATP)以既精准又高效?

ATP(Available To Promise)应做到“快 + 准”。建议分层:

  • 软检查用于快速响应,基于当前可用库存、已承诺数量和简单规则(例如优先分配、保留比),在用户下单瞬间给出反馈;
  • 硬检查会更准确,考虑在途采购、生产排产、外协交期和装配能力,它通常是异步任务并把结果回写到订单看板,提示是否需要延期或触发采购。

硬检查可在低峰时段或通过队列批量执行,避免阻塞用户。关键是把软检作为用户交互的默认体验,硬检作为风险管理与自动化触发的依据,同时在页面上把“软检结果”和“硬检结果”分开展示,便于销售做判断与沟通。

FAQ 2:退货与换货流程怎样联动质检与财务,避免库存与账务错配?

退货流程必须把“质检”作为决定库存动作前的必须步骤:

  • 客户发起退货后系统生成退货单并通知仓库/质检。
  • 质检人员在系统记录检验结论(可入库/返修/报废)并上传照片/检测报告;
  • 只有判定为“可入库”时才正式把物料增加到可用库存并触发财务的入库凭证;
  • 若为“报废”,则生成报废单并触发成本冲销/损失记账。
  • 换货要区分“先退后发”和“先发后退”两种操作流程,并把运费或维修费用的分摊规则在系统中固化。

所有步骤都要有审计日志(谁、何时、结果、附件),以便处理争议。技术上,退货入库应是事务性写入,质检结论驱动库存与会计凭证的生成,确保库存与账务并行一致或有可回溯的补偿流程。

FAQ 3:销售管理模块如何与生产(排产)、采购系统高效协同以降低交期风险?

在 MTO 场景下,销售订单往往直接触发生产与采购。最佳实践是通过事件驱动将订单信息以标准事件(如 order.created、order.updated)广播给库存、采购和排产服务:

  • 库存先尝试锁定关键件,若失败会发布 stock.lock_failed 事件并在看板标记风险;
  • 采购服务订阅事件并根据物料缺口生成采购建议、询价或加急单;
  • 排产服务根据优先级与交期将订单纳入计划并返回可执行的产能承诺。

这样可以避免同步阻塞、提高系统鲁棒性。关键点是设计清晰的事件契约(payload 结构、幂等键、重试策略),并保证事件处理的幂等性与可追踪性。

同时,建立反馈循环:当采购或排产发现无法达成承诺,应立刻把信息回写给销售看板并触发人工或自动化的补救(如调整交期或更换供应商),从而把交期风险在最早阶段暴露并处理。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 什么是 ERP(离散制造 - MTO)系统,销售板块为什么重要
  • 2.高层架构
  • 3. 关键业务流程
  • 4. 功能拆解
    • 4.1 销售管理看板
    • 4.2 报价单
    • 4.3 销售订单
    • 4.4 销售出库
    • 4.5 销售退货
    • 4.6 销售换货
    • 4.7 报表与统计
  • 5. 数据模型
  • 6. 接口与事件设计
    • 6.1 关键 REST API
    • 6.2 事件
  • 7. 开发技巧与工程实践
    • ATP 设计
    • 幂等与并发
    • 事件驱动
    • 审计与可追溯
    • 性能
    • 测试
    • 8. 代码实现
      • 数据库示例(Postgres)
      • 前端 React 示例(创建报价并确认)
  • 9. 验收标准与交付建议
  • 10. 开发中常见问题与解决思路
  • 11. 结论
  • FAQ
    • FAQ 1:在 MTO 场景下,如何设计可用性检查(ATP)以既精准又高效?
    • FAQ 2:退货与换货流程怎样联动质检与财务,避免库存与账务错配?
    • FAQ 3:销售管理模块如何与生产(排产)、采购系统高效协同以降低交期风险?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档