ERPNext Production Plan Sales Order 子表解析:销售订单如何进入生产计划工具

原创2026-09-30 13:52:531,464 阅读
文章标签:后端企业应用

ERPNext Production Plan Sales Order 子表解析:销售订单如何进入生产计划工具

导读

本文围绕 ERPNext 制造模块中 Production Plan Sales Order(生产计划销售订单) 这一子表 DocType 展开,它是「销售订单 → 生产计划(Production Plan)」数据流的关键桥接载体。通过本文,你将掌握该子表的字段定义与模型实现、开放销售订单的筛选逻辑、数据如何被灌入子表、保存时的校验机制,以及子表行如何进一步驱动生产计划行项目(Production Plan Item)的生成,可直接用于理解或二次开发生产排产相关的功能链路。

一、定位:README 的一句话定义

仓库中该 DocType 的 README 只有一句话:

Sales Order being considered for the Production Planning Tool. (被生产计划工具纳入考虑范围的销售订单。)

这句话精确定义了该子表的设计意图:它不是一个独立的业务单据,而是 Production Plan(生产计划)主文档内部的一张子表,用于暂存那些「被生产计划工具纳入考虑范围」的销售订单清单。生产计划人员在创建/编辑 Production Plan 时,通过此表确认哪些销售订单需要纳入本次排产,随后系统才据此拆分出具体要生产的物料行项目。从数据模型看,它与 Production Plan 之间是典型的父子表关系(parent / parentfield / parenttype 三字段),本身不具备独立命名系列,采用 autoname: "hash" 的随机命名规则,其 istable: 1 标志也直接印证了「子表」身份。

二、子表结构与字段详解

2.1 字段定义(JSON 元数据)

依据 production_plan_sales_order.json 中的 fields 定义,子表共包含 5 个业务字段,核心字段与约束如下:

字段名 字段类型 必填 可编辑 说明
sales_order Link → Sales Order 是(reqd: 1) 是 被纳入生产计划的销售订单主键,列表视图中展示
sales_order_date Date 否 只读 销售订单的交易日期(旧字段名 document_date)
customer Link → Customer 否 只读 对应销售订单的客户
grand_total Currency 否 只读 销售订单金额(按基础货币计)
status Data 否 可编辑 销售订单状态,列表视图中展示

值得注意的设计细节:

  • sales_order 是唯一允许用户手动编辑的输入字段,且 in_list_view: 1,意味着用户主要是在表格视图中选择/追加销售订单;
  • sales_order_date、customer、grand_total 三个字段全部 read_only: 1,由系统在拉取销售订单时自动回填,保证数据与源单据一致,避免人为篡改;
  • 布局上通过 col_break1 将字段分为两列,属于 Frappe 标准的表格列断行用法;
  • status 为普通 Data 类型,仅做展示,不驱动业务逻辑(真正的状态判定发生在销售订单本身)。

2.2 模型类实现

production_plan_sales_order.py 中模型类 ProductionPlanSalesOrder(Document) 的主体是 Frappe 自动生成的类型标注块(DF.Link、DF.Date、DF.Currency、DF.Data 等),运行逻辑体为 pass,即该子表本身不包含任何自定义业务方法。这符合子表「薄数据层」的设计惯例——真正的业务逻辑全部收敛在生产计划主文档及其服务层中,子表只承担数据承载。

三、数据流入:开放销售订单如何被装入子表

子表的数据由 Production Plan 的「从销售订单获取项目」功能驱动。在 production_plan.py 中:

def get_open_sales_orders(self):
    return SalesOrderSourcingService(self).get_open_sales_orders()

def add_so_in_table(self):
    return SalesOrderSourcingService(self).add_so_in_table(open_so)

实际执行逻辑位于 sales_order_planning.py 的 SalesOrderSourcingService:

  1. get_open_sales_orders() 调用 get_sales_orders(self.doc) 查询符合条件的开放销售订单;
  2. 若有结果,add_so_in_table() 会先 self.doc.set("sales_orders", []) 清空旧行,再逐条 append 新行,回填四个关键字段:
self.doc.append(
    "sales_orders",
    {
        "sales_order": data.name,
        "sales_order_date": data.transaction_date,
        "customer": data.customer,
        "grand_total": data.base_grand_total,
    },
)

这里可以看到 grand_total 实际取自销售订单的 base_grand_total(基础货币金额),而 sales_order_date 取自 transaction_date,与子表 JSON 中的字段类型完全对应。

3.1 「开放销售订单」的判定条件

查询构造在 planning_queries.py 的 get_sales_orders() 与 _open_so_base_query() 中,一个销售订单要被纳入子表,必须同时满足:

  • docstatus == 1:销售订单已提交;
  • status 不属于 Stopped、Closed:订单未被停止或关闭;
  • company 与生产计划一致;
  • 存在未纳入生产计划的量:行项目满足 so_item.qty > so_item.production_plan_qty(即还有数量未排产);
  • 存在可生产的依据:销售订单行项目对应有 is_active == 1 的有效 BOM,或其打包件(Packed Item)行存在有效 BOM(ExistsCriterion 子查询实现)。

3.2 可选过滤条件

_apply_open_so_filters() 进一步支持按日期与维度过滤:

  • 销售订单交易日期区间:from_date / to_date;
  • 行项目交货日期区间:from_delivery_date / to_delivery_date;
  • 按 customer(客户)、project(项目)精确过滤;
  • 按 sales_order_status 过滤,映射到销售订单自身的 status 字段(可选值如 To Deliver and Bill、To Bill、To Deliver)。

这些过滤条件对应生产计划界面上的筛选输入项,用户可据此只拉取符合排产窗口的订单。

四、保存校验:防止「无料可产」的订单混入

Production Plan 在 validate() 阶段会调用 validate_sales_orders(),对子表中的每个销售订单做二次确认:

data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
...
for sales_order in sales_orders:
    if sales_order not in data:
        frappe.throw(_("No items are available in the sales order {0} for production").format(sales_order))

其底层查询 sales_order_query(@frappe.whitelist(),白名单接口)同样限定 table.qty > table.production_plan_qty 且 table.docstatus == 1,并支持按 company、指定 sales_orders 列表、item_code、模糊文本 txt 过滤。换言之,即使用户手动向子表追加了一个销售订单,只要该订单没有任何可排产的行项目(数量已全部排完或未提交),保存时也会被拦截并抛出「No items are available in the sales order ... for production」。该校验既覆盖「点击拉取」的自动流程,也覆盖「手动选择」的编辑场景。

五、下游消费:从子表行到生产计划行项目

子表不是终点。当 get_items_from == "Sales Order" 时,get_so_items() 从子表读取销售订单清单:

so_list = self.get_so_mr_list("sales_order", "sales_orders")
items = self._so_items(so_list)
packed_items = self._so_packed_items(so_list)
self.add_items(items + packed_items)

_so_items() 对每个行项目计算 pending_qty(待排产数量):

item.pending_qty = flt(item.qty) - max(item.work_order_qty, flt(item.delivered_qty) * item.conversion_factor, 0)

即尚未下达工单且尚未交付的数量,再经 add_items() 与 _append_po_item() 生成 po_items(Production Plan Item)行,回填 sales_order、sales_order_item、planned_qty、pending_qty、planned_start_date 等,并调用 calculate_total_planned_qty() 汇总总计划数量。若启用 combine_items,则同一 BOM 的多张销售订单数量会被合并(combine_so_items() / _add_combine_ref()),合并明细写入 prod_plan_references 子表。至此完成「销售订单 → 子表 → 生产计划行项目」的完整转化。

六、测试验证与参考路径

test_production_plan.py 中提供了多条与该子表直接相关的用例:

  • test_production_plan_sales_orders(约 325 行):直接构造包含 sales_orders 子表行的生产计划,验证拉取与校验流程;
  • 多处断言 production_plan.get_open_sales_orders() 之后 pln.sales_orders[0].sales_order == so.name(约 727、1054、1066、1164 行),验证开放订单能被正确装入子表首行;
  • test_projected_qty_cascading_across_multiple_sales_orders(约 218 行)验证跨多张销售订单的预测数量级联计算。

这些测试覆盖了「拉取 → 回填 → 校验 → 生成行项目」的主链路,是理解该子表行为的最佳入口。核心源码参考:production_plan_sales_order.json、production_plan_sales_order.py、sales_order_planning.py、planning_queries.py、mapper.py、production_plan.py。

七、小结

Production Plan Sales Order 虽是一个「一句话 README」定义的薄子表,却在 ERPNext 制造排产链路中承担着承上启下的角色:向下承接销售订单中仍可排产的行项目,向上为生产计划行项目(PO Items)提供输入清单。理解其字段约束(必填的 sales_order、只读的日期/客户/金额)、开放订单判定条件(已提交、未停止/关闭、有剩余可排数量、具备有效 BOM)以及保存校验与消费逻辑,是深入生产计划模块、乃至自行扩展排产筛选逻辑的前提。

登录后查看全文
erpnext