InvenTree Build Output 管理实践:创建、序列号分配、部分完工、报废与取消
【免费下载链接】InvenTreeOpen Source Inventory Management System项目地址: https://gitcode.com/GitHub_Trending/in/InvenTree
在 InvenTree 的制造体系中,Build Order(生产订单)负责描述"要生产什么、生产多少",而真正落到实物上的每一批产出则由 Build Output(构建产出)来承载。本文以官方文档docs/docs/manufacturing/output.md为主线,结合后端应用src/backend/InvenTree/build/的模型、序列化器与 API 实现,系统讲解 Build Output 的完整生命周期:如何创建(含序列号与批号规则)、如何完工入库、如何部分完工/部分报废、如何报废与取消,以及这些操作在源码层是如何被校验和执行(含后台任务)的。读完本文,你既能按照界面流程管理生产产出,也能通过 API 与源码理解其底层数据模型。
一、什么是 Build Output:本质是"在生产中"的库存项
文档对 Build Output 的定义是:相对于一个 生产订单,Build Output 是该订单预期产出的成品(finished product)。其核心规则有:
- 一个生产订单可以拥有多个 Build Output,它们可以在不同时间、由不同操作员产出;
- 单个 Build Output 可以是一个单独的单位,也可以是一批(batch)单位;
- Build Output 可以关联序列号(serial numbers)和批号(batch code)。
在源码层面,这一概念有非常明确的落地方式。构建产出的序列化器BuildOutputSerializer(serializers.py)中有一句关键注释:
Note that a "BuildOutput" is really just a StockItem which is "in production"!
也就是说,Build Output 并不是独立的数据模型,而是一个带有is_building=True标记的 StockItem(库存项)。这也解释了文档中提到的一个重要约束:未完工的 Build Output 本质上仍是库存项,在完成(complete)之前可以被拆分、分配序列号等——它只是处于"在生产"状态。而生产订单与库存项之间的物料分配关系则由BuildItem模型维护,其定义见 models.py:BuildItem将多个 StockItem 关联到一个 Build,并记录数量与装配目标(install_into)。
界面上,生产订单页面用两个标签页组织产出:
- Incomplete Outputs(未完工产出):显示当前订单所有 outstanding / 进行中的 Build Output;
- Completed Outputs(已完成产出):显示已被 完工 或 报废 的产出。
二、创建 Build Output
在 Incomplete Outputs 标签页下点击 "New Build Output" 按钮即可创建新产出。创建时可用选项如下(继承自文档的完整参数表):
| 选项 | 说明 |
|---|---|
| Quantity(数量) | 本次构建产出要创建的物品数量 |
| Serial Numbers(序列号) | 为生成的产出指定可选序列号;是否可留空见下文 |
| Batch Code(批号) | 生成产出的批次标识 |
| Auto Allocate Serial Numbers(自动分配序列号) | 勾选后,已具有序列号的可用受跟踪子件会被自动分配到序列号匹配的构建产出上 |
2.1 序列号输入与"无序列号"批产
序列号的输入格式(范围、逗号分隔、自动生成等)参考 序列号生成指南。
一个容易被忽略但非常实用的能力是:即使被生产的部件被标记为 可跟踪(trackable),创建 Build Output 时也不强制要求序列号。留空序列号字段时,系统会按指定的 Quantity 创建单个(非序列化的)Build Output,而不是每个单位一个。这样可以在生产前端一次性产出整批数量——例如一批 PCB 在制造后期才逐个序列化,或者 外部生产订单 收回但尚未序列化的单位——之后再通过拆分(split)为其分配序列号,最后再 完工。
需要注意一个例外(文档以 note 强调):如果所生产部件的 BOM 中包含受跟踪的子件,创建构建产出时仍然必须提供序列号,详见 受跟踪构建产出 一节。
2.2 源码印证:create_build_output 的校验逻辑
后端实现在Build模型的create_build_output方法中(models.py)。其签名接受quantity以及batch、serials、location、auto_allocate等关键字参数,与界面选项一一对应。关键逻辑有:
- 当
location未指定时,回退为订单的destination(目标库位)或部件默认库位; - 校验
if self.part.has_trackable_parts and not serials: raise ValidationError(...)—— 即部件的 BOM 含受跟踪子件而未提供序列号时直接报错,这正是文档中 "Tracked BOM Items" 提示的代码依据; - 若提供了序列号,调用
StockItem._create_serial_numbers(...)为每个序列号各创建一个quantity=1、is_building=True的库存项,并为每个产出写入StockHistoryCode.BUILD_OUTPUT_CREATED跟踪记录; - 勾选 Auto Allocate 时,逐个产出调用
auto_allocate_tracked_output,把库中序列号匹配的受跟踪子件自动装配到对应产出上。
创建产出的 API 端点为BuildOutputCreate(api.py),使用BuildOutputCreateSerializer(serializers.py)完成输入校验,成功后以 201 返回创建出的 StockItem 列表。
三、库存分配与 Build Output 的关系(前置概念)
理解完工/报废动作前,需要知道分配(allocation)是如何挂接到具体产出上的。物料分配文档中的 "Allocating tracked stock" 描述:受跟踪物料(有序列号的子件)在分配时可以指定install_into(装配目标),即某个具体的 Build Output。源码中BuildItem的install_into字段(models.py)承载了这一关系,而output.items_to_install查询集则返回所有指向某个产出的BuildItem。完工与报废操作的核心差异,正体现在这些"待装配"的 BuildItem 上如何处理。
四、完工 Build Output
"完工"(Complete)将某个 Build Output 标记为在该生产订单语境下的成品。操作入口是选中该产出对应的 "Complete build output" 按钮,此时可选:
| 选项 | 说明 |
|---|---|
| Status(状态) | 完工产出对应的 库存状态 |
| Location(库位) | 产出所在的 库存库位 |
| Notes(备注) | 与本次完工相关的附加备注 |
| Accept Incomplete Allocation(接受未完成分配) | 勾选后,允许在必需 BOM 项未完全分配的情况下完工 受跟踪构建产出 |
文档明确列出完工会执行的动作:
- 订单的 completed 数量增加所选产出的数量;
- 产出被标记为 "completed",此后可用于库存操作;
- 分配到该产出的 受跟踪 BOM 物料 被安装(install)进该产出。
4.1 源码纵深:complete_build_output 的完整执行链
模型方法complete_build_output(models.py)是一个@transaction.atomic方法,执行链可归纳为:
- 前置校验
can_complete_output(models.py):- 产出必须仍处于
is_building状态,且属于该订单(防止并发/重复任务把已完工产出二次完工); - 若全局设置
PREVENT_BUILD_COMPLETION_HAVING_INCOMPLETED_TESTS开启,则要求产出已通过全部必需测试(passedAllRequiredTests); - 已分配且自身仍"在生产"的物料不允许被装配进正在完工的产出;
- 部分完工约束:指定小于产出总量的数量时,若该产出名下已有分配项(
items_to_install非空),则禁止拆分——这从源码角度解释了为什么序列化(必然已逐件分配子件)的产出不能部分完工。
- 产出必须仍处于
- 部分完工拆分:若
quantity != output.quantity,先调用output.splitStock(quantity, user=user, allow_production=True)把原产出拆成两个,完成数量对应的那一半,另一半仍留在 Incomplete Outputs 中等待后续完工。这与文档 "Partial Completion" 一节描述的"一分为二"行为完全一致。 - 装配受跟踪物料:对
output.items_to_install全部 BuildItem 执行complete_allocations,随后删除这些 BuildItem 记录(子件库存被消耗、正式装入成品)。 - 状态落地:
is_building=False、写入location(未指定时回退为订单destination)、set_status(status)(默认StockStatus.OK),并写入StockHistoryCode.BUILD_OUTPUT_COMPLETED跟踪记录。 - 事件与计数:触发
BuildEvents.OUTPUT_COMPLETED事件(供插件订阅),并用数据库级原子增量self.completed = F('completed') + output.quantity增加完工数量——源码注释说明这样是为了避免多个产出被并发完工时的"丢失更新"。
值得注意的工程细节:API 层的BuildOutputComplete端点(api.py)并不同步执行上述逻辑,而是通过offload_task把complete_build_outputs任务(tasks.py)投递给后台 worker,并立即返回TaskDetailSerializer响应。BuildOutputScrap端点(api.py)同样采用该机制(任务见 tasks.py)。这意味着通过 API 发起完工/报废是异步的,客户端需要轮询任务状态确认结果。
五、报废 Build Output
"报废"(Scrap)将产出标记为在该订单语境下的拒收(rejected),同样通过选中该产出的 "Scrap build output" 按钮操作。报废会执行:
- 产出被标记为 "rejected" 并从生产订单中移除;
- 订单的完工数量不增加;
- 报废的产出不可再进行任何库存操作;
- 可选地,分配到该产出的 受跟踪 BOM 物料 可被安装进该报废产出。
报废可用选项:
| 选项 | 说明 |
|---|---|
| Location(库位) | 报废产出所在的库存库位 |
| Notes(备注) | 与本次报废相关的附加备注 |
| Discard Allocations(丢弃分配) | 勾选后,已装配的 BOM 物料会先被拆除(回到可用库存),再把产出标记为报废;适用于装配物可回收、可挪作他用的场景 |
5.1 源码印证:scrap_build_output 与 discard_allocations
scrap_build_output方法(models.py)与完工逻辑形成对照:
- 同样做状态/归属/数量合法性校验;
- 数量小于产出总量时同样走
splitStock拆分(部分报废); - 关键差异在于:报废分支中
output.status = StockStatus.REJECTED,且没有任何self.completed += ...语句——完工数量确实不增加; - 对
discard_allocations的处理非常精确:if not discard_allocations: self.complete_allocations(...),即默认把已分配物料"安装"进报废品;勾选丢弃分配时跳过安装步骤,随后allocated_items.all().delete()使这些子件释放回可用库存。这与文档 "Discard Allocations" 的语义("installed items are recoverable and can be used elsewhere")逐字对应; - 最后写入
StockHistoryCode.BUILD_OUTPUT_REJECTED跟踪记录,记录数量、库位、状态与所属订单。
5.2 部分完工与部分报废
两者支持相同的"部分量"语义,且都有相同的限制:
- 部分完工:指定小于产出总量的数量时,产出被拆为两个 Build Output——指定数量标记为完工并计入完工数量,剩余数量留在 Incomplete Outputs 中;
- 部分报废:产出同样被拆为两个,指定数量标记为报废(完工数量不增加),剩余数量保留待后续处理;
- 序列化产出既不能部分完工,也不能部分报废(文档两处 note 均强调)。从源码结构看,序列化产出每个数量对应一个独立 StockItem(见 2.2 节),其"拆分"在模型层面没有意义,因此该限制是数据模型的自然推论;此外 4.1 节提到的"有分配项禁止拆分"校验从实现上进一步封死了这条路径。
相关行为在测试中也有覆盖,例如部分完工(quantity=1拆分)、数量为 0 的非法输入等用例可见 test_build.py。
六、取消 Build Output
"取消"(Cancel)与"报废"有本质区别:取消会把 Build Output 从数据库中彻底删除,适用于该产出在物理上不存在(或从未被生产)而不应被数据库跟踪的场景。
取消执行的动作:
- 已分配给该产出的库存项被返还库存(deallocate);
- 该 Build Output 从数据库中被移除。
源码对应cancel_build_output逻辑(models.py):先校验产出处于is_building状态且归属当前订单,然后self.deallocate_stock(output=output)释放所有分配,最后以output.delete(ignore_serial_check=True)删除库存项。源码注释特别说明这是一个特例:序列号库存通常受全局设置保护不可删除,但取消构建产出时允许越过该检查——因为产出尚未完工入库,其序列号不应占用系统序列号空间。对应的 API 端点为BuildOutputDelete(api.py),序列化器BuildOutputDeleteSerializer见 serializers.py。
七、状态机小结与 API 入口一览
把整个生命周期串起来,一个 Build Output(即is_building=True的 StockItem)的状态迁移为:
| 操作 | 产出结果 | 完工数量 | 数据库记录 | 跟踪事件 |
|---|---|---|---|---|
| 创建 | is_building=True,位于订单 destination 或默认库位 | 不变 | 保留 | BUILD_OUTPUT_CREATED |
| 完工(含部分完工) | is_building=False,状态按所选(默认 OK),进入普通库存 | 增加 | 保留 | BUILD_OUTPUT_COMPLETED+BuildEvents.OUTPUT_COMPLETED |
| 报废(含部分报废) | is_building=False,状态 REJECTED,不可再操作库存 | 不增加 | 保留 | BUILD_OUTPUT_REJECTED |
| 取消 | 记录被删除,分配返还库存 | 不变 | 删除(越过序列化删除保护) | — |
API 入口均挂载在生产订单资源下(见 build/api.py):BuildOutputCreate(创建,同步返回)、BuildOutputComplete与BuildOutputScrap(完工/报废,异步后台任务,返回任务详情)、BuildOutputDelete(取消),以及查询用的BuildItemDetail(api.py)。所有输入经由 serializers.py 中的BuildOutputCreateSerializer/BuildOutputCompleteSerializer/BuildOutputScrapSerializer/BuildOutputDeleteSerializer校验,完工/报废支持对outputs列表按项指定quantity以实现部分完工/报废。
八、结语
InvenTree 的 Build Output 设计体现了一个务实的思路:不引入独立于库存体系的"在制品"模型,而是用StockItem加is_building标记复用整套库存跟踪、状态、库位与序列号能力,同时通过BuildItem把 BOM 装配关系精确挂到单个产出上。由此得到的能力——批量先行生产后补序列化、部分完工/部分报废的自动拆分、报废时的分配安装或回收、取消时序列号的释放——都直接建立在这个数据模型之上。理解这一点后,无论是通过界面操作,还是通过 REST API 与后台任务集成自有生产系统,行为都将是可预期、可验证的。
【免费下载链接】InvenTreeOpen Source Inventory Management System项目地址: https://gitcode.com/GitHub_Trending/in/InvenTree
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考