ERPNext Production Plan Sales Order 子表解析:销售订单如何进入生产计划工具
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:
get_open_sales_orders()调用get_sales_orders(self.doc)查询符合条件的开放销售订单;- 若有结果,
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)以及保存校验与消费逻辑,是深入生产计划模块、乃至自行扩展排产筛选逻辑的前提。